]> dgit.raspbian.org Git - dovecot.git/commitdiff
Import dovecot_2.4.1+dfsg1-6+deb13u7.debian.tar.xz
authorNoah Meyerhans <noahm@debian.org>
Wed, 16 Sep 2026 19:06:35 +0000 (15:06 -0400)
committerNoah Meyerhans <noahm@debian.org>
Wed, 16 Sep 2026 19:06:35 +0000 (15:06 -0400)
[dgit import tarball dovecot 1:2.4.1+dfsg1-6+deb13u7 dovecot_2.4.1+dfsg1-6+deb13u7.debian.tar.xz]

298 files changed:
autogen.sh [new file with mode: 0755]
changelog [new file with mode: 0644]
conf/conf.d/10-auth.conf [new file with mode: 0644]
conf/conf.d/10-logging.conf [new file with mode: 0644]
conf/conf.d/10-mail.conf [new file with mode: 0644]
conf/conf.d/10-master.conf [new file with mode: 0644]
conf/conf.d/10-metrics.conf [new file with mode: 0644]
conf/conf.d/10-ssl.conf [new file with mode: 0644]
conf/conf.d/15-lda.conf [new file with mode: 0644]
conf/conf.d/15-mailboxes.conf [new file with mode: 0644]
conf/conf.d/20-imap.conf [new file with mode: 0644]
conf/conf.d/20-lmtp.conf [new file with mode: 0644]
conf/conf.d/20-managesieve.conf [new file with mode: 0644]
conf/conf.d/20-pop3.conf [new file with mode: 0644]
conf/conf.d/20-submission.conf [new file with mode: 0644]
conf/conf.d/30-dict-server.conf [new file with mode: 0644]
conf/conf.d/90-acl.conf [new file with mode: 0644]
conf/conf.d/90-fts-flatcurve.conf [new file with mode: 0644]
conf/conf.d/90-fts.conf [new file with mode: 0644]
conf/conf.d/90-quota.conf [new file with mode: 0644]
conf/conf.d/90-sieve-extprograms.conf [new file with mode: 0644]
conf/conf.d/90-sieve.conf [new file with mode: 0644]
conf/conf.d/99-local.conf [new file with mode: 0644]
conf/conf.d/auth-deny.conf.ext [new file with mode: 0644]
conf/conf.d/auth-ldap.conf.ext [new file with mode: 0644]
conf/conf.d/auth-master.conf.ext [new file with mode: 0644]
conf/conf.d/auth-oauth2.conf.ext [new file with mode: 0644]
conf/conf.d/auth-passwdfile.conf.ext [new file with mode: 0644]
conf/conf.d/auth-sql.conf.ext [new file with mode: 0644]
conf/conf.d/auth-static.conf.ext [new file with mode: 0644]
conf/conf.d/auth-system.conf.ext [new file with mode: 0644]
conf/dovecot.conf [new file with mode: 0644]
conf/local.conf [new file with mode: 0644]
control [new file with mode: 0644]
copyright [new file with mode: 0644]
dh.pem [new file with mode: 0644]
dovecot-core.NEWS [new file with mode: 0644]
dovecot-core.README.Debian [new file with mode: 0644]
dovecot-core.bug-control [new file with mode: 0644]
dovecot-core.bug-script [new file with mode: 0644]
dovecot-core.dirs [new file with mode: 0644]
dovecot-core.docs [new file with mode: 0644]
dovecot-core.dovecot.default [new file with mode: 0644]
dovecot-core.dovecot.init [new file with mode: 0644]
dovecot-core.dovecot.pam [new file with mode: 0644]
dovecot-core.install [new file with mode: 0644]
dovecot-core.lintian-overrides [new file with mode: 0644]
dovecot-core.manpages [new file with mode: 0644]
dovecot-core.postinst [new file with mode: 0644]
dovecot-core.postrm [new file with mode: 0644]
dovecot-core.triggers [new file with mode: 0644]
dovecot-dev.README.Debian [new file with mode: 0644]
dovecot-dev.lintian-overrides [new file with mode: 0644]
dovecot-flatcurve.links [new file with mode: 0644]
dovecot-flatcurve.postinst [new file with mode: 0644]
dovecot-flatcurve.postrm [new file with mode: 0644]
dovecot-flatcurve.prerm [new file with mode: 0644]
dovecot-flatcurve.triggers [new file with mode: 0644]
dovecot-gssapi.links [new file with mode: 0644]
dovecot-imapd.links [new file with mode: 0644]
dovecot-imapd.lintian-overrides [new file with mode: 0644]
dovecot-imapd.postinst [new file with mode: 0644]
dovecot-imapd.postrm [new file with mode: 0644]
dovecot-imapd.prerm [new file with mode: 0644]
dovecot-imapd.triggers [new file with mode: 0644]
dovecot-imapd.ufw.profile [new file with mode: 0644]
dovecot-ldap.links [new file with mode: 0644]
dovecot-ldap.postinst [new file with mode: 0644]
dovecot-ldap.postrm [new file with mode: 0644]
dovecot-ldap.triggers [new file with mode: 0644]
dovecot-lmtpd.links [new file with mode: 0644]
dovecot-lmtpd.postinst [new file with mode: 0644]
dovecot-lmtpd.postrm [new file with mode: 0644]
dovecot-lmtpd.prerm [new file with mode: 0644]
dovecot-lmtpd.triggers [new file with mode: 0644]
dovecot-managesieved.README.Debian [new file with mode: 0644]
dovecot-managesieved.install [new file with mode: 0644]
dovecot-managesieved.links [new file with mode: 0644]
dovecot-managesieved.lintian-overrides [new file with mode: 0644]
dovecot-managesieved.postinst [new file with mode: 0644]
dovecot-managesieved.postrm [new file with mode: 0644]
dovecot-managesieved.prerm [new file with mode: 0644]
dovecot-managesieved.triggers [new file with mode: 0644]
dovecot-mysql.links [new file with mode: 0644]
dovecot-mysql.triggers [new file with mode: 0644]
dovecot-openssl.cnf [new file with mode: 0644]
dovecot-pgsql.links [new file with mode: 0644]
dovecot-pgsql.triggers [new file with mode: 0644]
dovecot-pop3d.links [new file with mode: 0644]
dovecot-pop3d.postinst [new file with mode: 0644]
dovecot-pop3d.postrm [new file with mode: 0644]
dovecot-pop3d.prerm [new file with mode: 0644]
dovecot-pop3d.triggers [new file with mode: 0644]
dovecot-pop3d.ufw.profile [new file with mode: 0644]
dovecot-sieve.links [new file with mode: 0644]
dovecot-sieve.lintian-overrides [new file with mode: 0644]
dovecot-sieve.postinst [new file with mode: 0644]
dovecot-sieve.postrm [new file with mode: 0644]
dovecot-sieve.triggers [new file with mode: 0644]
dovecot-solr.README.Debian [new file with mode: 0644]
dovecot-solr.install [new file with mode: 0644]
dovecot-solr.links [new file with mode: 0644]
dovecot-solr.triggers [new file with mode: 0644]
dovecot-sqlite.links [new file with mode: 0644]
dovecot-sqlite.triggers [new file with mode: 0644]
dovecot-submissiond.postinst [new file with mode: 0644]
dovecot-submissiond.postrm [new file with mode: 0644]
dovecot-submissiond.prerm [new file with mode: 0644]
dovecot-submissiond.triggers [new file with mode: 0644]
gbp.conf [new file with mode: 0644]
maildirmake.dovecot [new file with mode: 0644]
maildirmake.dovecot.1 [new file with mode: 0644]
patches/0001-auth-db-oauth2-Reduce-nesting-in-token_in_scope-via-.patch [new file with mode: 0644]
patches/0001-commin-Rename-LOGIN_PROXY_FAILURE_TYPE_AUTH-to-LOGIN.patch [new file with mode: 0644]
patches/0001-dsync-Avoid-potentially-excessive-data-stack-growth-.patch [new file with mode: 0644]
patches/0001-global-Fix-spelling.patch [new file with mode: 0644]
patches/0001-imap-Extract-side-channel-ostream-creation-into-help.patch [new file with mode: 0644]
patches/0001-imap-Restrict-COMPRESS-to-DEFLATE-only.patch [new file with mode: 0644]
patches/0001-imap-hibernate-Fix-out-of-bounds-read-when-parsing-D.patch [new file with mode: 0644]
patches/0001-imap-login-Add-comments-to-internal-ID-command-param.patch [new file with mode: 0644]
patches/0001-imap-login-Fix-excessive-memory-growth-with-pre-logi.patch [new file with mode: 0644]
patches/0001-lda-Fix-using-USER-environment-if-d-hasn-t-been-spec.patch [new file with mode: 0644]
patches/0001-lib-Add-XXH64-hash-implementation.patch [new file with mode: 0644]
patches/0001-lib-Add-str_equals_timing_safe.patch [new file with mode: 0644]
patches/0001-lib-compression-Don-t-calculate-crc32-if-using-zlib-.patch [new file with mode: 0644]
patches/0001-lib-compression-istream-lz4-Ensure-uncompressed-chun.patch [new file with mode: 0644]
patches/0001-lib-compression-istream-lz4-Try-again-if-no-data-was.patch [new file with mode: 0644]
patches/0001-lib-compression-istream-zlib-Use-container_of-macro.patch [new file with mode: 0644]
patches/0001-lib-compression-wrap-_read-in-for-loop-reindent-only.patch [new file with mode: 0644]
patches/0001-lib-i_close_fd-Document-that-it-preserves-errno.patch [new file with mode: 0644]
patches/0001-lib-imap-imap-match-Fix-excessive-CPU-usage-caused-b.patch [new file with mode: 0644]
patches/0001-lib-imap-urlauth-Fix-leaking-uninitialized-memory-in.patch [new file with mode: 0644]
patches/0001-lib-lda-mail-deliver-Autocreate-delivery-mailbox-via.patch [new file with mode: 0644]
patches/0001-lib-mail-Reset-charset-translation-buffer-between-MI.patch [new file with mode: 0644]
patches/0001-lib-mail-o_stream_dot_sendv-Use-I_MIN.patch [new file with mode: 0644]
patches/0001-lib-mail-ostream-dot-Optimize-stream-writing.patch [new file with mode: 0644]
patches/0001-lib-managesieve-managesieve-parser-Fix-handling-of-l.patch [new file with mode: 0644]
patches/0001-lib-sieve-Add-per-user-sieve-rusage-file-for-cumulat.patch [new file with mode: 0644]
patches/0001-lib-sieve-actions-Split-mailbox-creation-into-sieve_.patch [new file with mode: 0644]
patches/0001-lib-sieve-sieve-binary-code-Fix-single-byte-oob-stac.patch [new file with mode: 0644]
patches/0001-lib-sieve-sieve-storage-Add-is_personal-bit.patch [new file with mode: 0644]
patches/0001-lib-sieve-sieve-storage-Fix-panic-caused-by-flawed-d.patch [new file with mode: 0644]
patches/0001-lib-sieve-storage-file-Refuse-symlinks-escaping-pers.patch [new file with mode: 0644]
patches/0001-lib-sieve-util-edit-mail-Fix-writing-to-freed-memory.patch [new file with mode: 0644]
patches/0001-lib-sql-Add-sql_result_new_error-helper.patch [new file with mode: 0644]
patches/0001-lib-sql-Use-strchr-based-while-loop-to-fill-template.patch [new file with mode: 0644]
patches/0001-lib-sql-driver-sqlite-Use-sqlite3_snprintf-to-quote-.patch [new file with mode: 0644]
patches/0001-lib-sql-sql-api-Implement-async-calls-for-drivers-th.patch [new file with mode: 0644]
patches/0001-lib-storage-Allow-NULL-path-in-mailbox_list_mkdir_ro.patch [new file with mode: 0644]
patches/0001-lib-storage-thread-Avoid-excessive-data-stack-growth.patch [new file with mode: 0644]
patches/0001-lib-test-Add-test_assert_memcmp.patch [new file with mode: 0644]
patches/0001-lib-test-Require-both-lengths-in-test_assert_memcmp.patch [new file with mode: 0644]
patches/0001-lib-xxh64-fix-byte-ordering-bug-on-big-endian-system.patch [new file with mode: 0644]
patches/0001-login-Add-LOGIN_PROXY_FAILURE_TYPE_AUTH_NOT_REPLIED.patch [new file with mode: 0644]
patches/0001-login-common-Fix-crash-when-XCLIENT-FORWARD-base64-c.patch [new file with mode: 0644]
patches/0001-login-common-client-common-Add-client_disconnect-vfu.patch [new file with mode: 0644]
patches/0001-login-common-login-Add-proxy_dest_connection_limit-e.patch [new file with mode: 0644]
patches/0001-managesieve-login-Adjust-to-core-s-login_proxy_failu.patch [new file with mode: 0644]
patches/0001-managesieve-login-Support-LOGIN_PROXY_FAILURE_TYPE_A.patch [new file with mode: 0644]
patches/0001-submission-Fix-settings-leak-on-error-paths-in-clien.patch [new file with mode: 0644]
patches/0001-trash-Use-mailbox-event-in-trash_try_mailbox-for-set.patch [new file with mode: 0644]
patches/0002-auth-Fix-prefixing-forward_fields-without-a-value-fr.patch [new file with mode: 0644]
patches/0002-auth-db-oauth2-Fix-scope-check-to-require-all-config.patch [new file with mode: 0644]
patches/0002-doveadm-Avoid-leaking-doveadm_password-or-doveadm_ap.patch [new file with mode: 0644]
patches/0002-doveadm-compress-connect-Don-t-hang-when-server-reje.patch [new file with mode: 0644]
patches/0002-dsync-Fix-escaping-mail-or-attribute-value-that-begi.patch [new file with mode: 0644]
patches/0002-imap-Recreate-multiplex-ostream-side-channel-after-u.patch [new file with mode: 0644]
patches/0002-imap-Stream-BODYSTRUCTURE-to-client-without-ostream-.patch [new file with mode: 0644]
patches/0002-imap-hibernate-Use-imap-parser-API-for-parsing-DONE-.patch [new file with mode: 0644]
patches/0002-lda-Default-mail_home-HOME-environment-if-not-using-.patch [new file with mode: 0644]
patches/0002-lib-Replace-str_hash-strcase_hash-with-xxh64.patch [new file with mode: 0644]
patches/0002-lib-compression-Iterate-instead-of-recursing-on-zero.patch [new file with mode: 0644]
patches/0002-lib-mail-o_stream_dot_sendv-Fix-boundary-off-by-one.patch [new file with mode: 0644]
patches/0002-lib-path-util-Add-t_openat_safe-for-jailed-openat-re.patch [new file with mode: 0644]
patches/0002-lib-sieve-Drop-binary-header-resource-usage-tracking.patch [new file with mode: 0644]
patches/0002-lib-sieve-actions-Create-fileinto-mailbox-without-th.patch [new file with mode: 0644]
patches/0002-lib-sieve-storage-file-Validate-script-stat-path-thr.patch [new file with mode: 0644]
patches/0002-lib-sieve-util-test-edit-mail-Adjust-to-changes-in-d.patch [new file with mode: 0644]
patches/0002-lib-sql-Extract-sql_query_callback_delayed-helper.patch [new file with mode: 0644]
patches/0002-lib-storage-Remove-MAILBOX_FLAG_AUTO_CREATE-and-MAIL.patch [new file with mode: 0644]
patches/0002-lib-storage-Simplify-mailbox_mkdir-using-mailbox_lis.patch [new file with mode: 0644]
patches/0002-lib-storage-thread-Limit-References-header-msgid-cou.patch [new file with mode: 0644]
patches/0002-login-common-Avoid-array_front-panic-on-empty-forwar.patch [new file with mode: 0644]
patches/0002-managesieve-login-client_skip_line-Discard-data-when.patch [new file with mode: 0644]
patches/0002-submission-login-client-Fix-panic-occurring-at-mail_.patch [new file with mode: 0644]
patches/0003-auth-db-oauth2-Add-oauth2_audience-setting-deprecate.patch [new file with mode: 0644]
patches/0003-doveadm-compress-Delay-failing-if-used-COMPRESS-algo.patch [new file with mode: 0644]
patches/0003-imap-Stream-ENVELOPE-to-client-without-ostream-memor.patch [new file with mode: 0644]
patches/0003-lib-Add-str_stable_hash.patch [new file with mode: 0644]
patches/0003-lib-Add-t_openat_safe_dir.patch [new file with mode: 0644]
patches/0003-lib-compression-Add-o_stream_deflate_reset_dict.patch [new file with mode: 0644]
patches/0003-lib-compression-istream-zstd-Guard-against-no-progre.patch [new file with mode: 0644]
patches/0003-lib-mail-o_stream_dot_sendv-Use-enumeration-to-defin.patch [new file with mode: 0644]
patches/0003-lib-sieve-Rename-on-disk-header-rusage-fields-to-unu.patch [new file with mode: 0644]
patches/0003-lib-sieve-edit_mail_headers_parse-Fix-info-leak-via-.patch [new file with mode: 0644]
patches/0003-lib-smtp-smtp-server-Expose-smtp_server_connection_r.patch [new file with mode: 0644]
patches/0003-lib-sql-Add-sql_commit_schedule_delayed-helper.patch [new file with mode: 0644]
patches/0003-lib-storage-thread-Limit-ancestor-chain-traversal-de.patch [new file with mode: 0644]
patches/0004-acl-dovecot-acl-list-is-never-supposed-to-be-a-symli.patch [new file with mode: 0644]
patches/0004-imap-login-Add-dict_reset-side-channel-command-to-im.patch [new file with mode: 0644]
patches/0004-lib-Key-str_hash-strcase_hash-with-a-random-seed-to-.patch [new file with mode: 0644]
patches/0004-lib-compression-test-Add-zero-len-frame-test-for-all.patch [new file with mode: 0644]
patches/0004-lib-mail-message-parser-from-parts-Enforce-50-MB-all.patch [new file with mode: 0644]
patches/0004-lib-mail-o_stream_dot_sendv-Do-not-send-unguarded-.-.patch [new file with mode: 0644]
patches/0004-lib-sieve-Switch-sieve_max_cpu_time-enforcement-to-t.patch [new file with mode: 0644]
patches/0004-lib-var-expand-Change-escape-func-signature-to-retur.patch [new file with mode: 0644]
patches/0004-submission-login-client-authenticate-Reply-421-4.7.0.patch [new file with mode: 0644]
patches/0005-imap-Add-imap_compress_on_proxy-hidden-setting.patch [new file with mode: 0644]
patches/0005-lib-Add-comments-about-memory-allocations-and-string.patch [new file with mode: 0644]
patches/0005-lib-index-mail-index-strmap-Fix-OOB-read-from-trunca.patch [new file with mode: 0644]
patches/0005-lib-mail-Add-count-field-to-struct-message_address_l.patch [new file with mode: 0644]
patches/0005-lib-sieve-file-storage-Remove-orphan-compiled-binari.patch [new file with mode: 0644]
patches/0005-lib-sql-Make-escape_string-return-int-with-error_r-f.patch [new file with mode: 0644]
patches/0005-submission-login-submission-proxy-Recognize-421-4.7..patch [new file with mode: 0644]
patches/0006-auth-passdb_sql-connect-before-expanding-query-varia.patch [new file with mode: 0644]
patches/0006-imap-Reset-DEFLATE-dictionary-after-every-tagged-rep.patch [new file with mode: 0644]
patches/0006-lib-index-Rename-crc32-variables-parameters-to-hash-.patch [new file with mode: 0644]
patches/0006-lib-sieve-Log-a-warning-when-cumulative-CPU-exceeds-.patch [new file with mode: 0644]
patches/0006-lib-storage-mbox-Explicitly-disable-the-header-block.patch [new file with mode: 0644]
patches/0007-lib-index-Add-keyed-xxh64-strmap-format-v2-gated-by-.patch [new file with mode: 0644]
patches/0007-lib-mail-istream-header-filter-Use-container_of-for-.patch [new file with mode: 0644]
patches/0007-lib-sieve-Log-an-info-line-when-opening-a-CPU-disabl.patch [new file with mode: 0644]
patches/0007-lib-sql-Run-sql-statement-queries-in-their-own-data-.patch [new file with mode: 0644]
patches/0008-doveadm-Fix-ubsan-unsigned-integer-overflow-error.patch [new file with mode: 0644]
patches/0008-lib-mail-Make-istream-header-filter-s-per-header-siz.patch [new file with mode: 0644]
patches/0008-lib-sieve-Reduce-CPU-limit-check-interval-in-contain.patch [new file with mode: 0644]
patches/0009-config-Assume-dovecot_config_version-0.0.0-is-the-sa.patch [new file with mode: 0644]
patches/0009-lib-storage-Cap-per-header-size-in-index_mail_get_he.patch [new file with mode: 0644]
patches/0010-lib-mail-Clamp-hdr-value-cumulatively-against-header.patch [new file with mode: 0644]
patches/0010-lib-master-Move-config_version_find-from-src-config-.patch [new file with mode: 0644]
patches/0011-lib-lib-master-Move-version_-to-lib-version.-ch.patch [new file with mode: 0644]
patches/0011-lib-mail-Introduce-struct-message_part_data_limits-a.patch [new file with mode: 0644]
patches/0012-config-doveconf-dF-Add-dovecot_storage_version.patch [new file with mode: 0644]
patches/0012-lib-mail-Limit-total-address-count-per-message-to-10.patch [new file with mode: 0644]
patches/0013-lib-mail-Limit-total-Content-Language-tag-count-per-.patch [new file with mode: 0644]
patches/0014-lib-mail-Limit-total-MIME-parameter-count-per-messag.patch [new file with mode: 0644]
patches/CVE-2025-59028.patch [new file with mode: 0644]
patches/CVE-2025-59031.patch [new file with mode: 0644]
patches/CVE-2025-59032.patch [new file with mode: 0644]
patches/CVE-2026-24031-27860-1.patch [new file with mode: 0644]
patches/CVE-2026-24031-27860-2.patch [new file with mode: 0644]
patches/CVE-2026-24031-27860-3.patch [new file with mode: 0644]
patches/CVE-2026-24031-27860-4.patch [new file with mode: 0644]
patches/CVE-2026-24031-27860-5.patch [new file with mode: 0644]
patches/CVE-2026-24031-27860-6.patch [new file with mode: 0644]
patches/CVE-2026-24031-27860-7.patch [new file with mode: 0644]
patches/CVE-2026-24031-27860-8.patch [new file with mode: 0644]
patches/CVE-2026-27851.patch [new file with mode: 0644]
patches/CVE-2026-27855-1.patch [new file with mode: 0644]
patches/CVE-2026-27855-2.patch [new file with mode: 0644]
patches/CVE-2026-27855-3.patch [new file with mode: 0644]
patches/CVE-2026-27855-4.patch [new file with mode: 0644]
patches/CVE-2026-27856-1.patch [new file with mode: 0644]
patches/CVE-2026-27856-2.patch [new file with mode: 0644]
patches/CVE-2026-27856-3.patch [new file with mode: 0644]
patches/CVE-2026-27857-1.patch [new file with mode: 0644]
patches/CVE-2026-27857-2.patch [new file with mode: 0644]
patches/CVE-2026-27857-3.patch [new file with mode: 0644]
patches/CVE-2026-27857-4.patch [new file with mode: 0644]
patches/CVE-2026-27857-5.patch [new file with mode: 0644]
patches/CVE-2026-27858.patch [new file with mode: 0644]
patches/CVE-2026-27859.patch [new file with mode: 0644]
patches/CVE-2026-33603.patch [new file with mode: 0644]
patches/CVE-2026-40016.patch [new file with mode: 0644]
patches/CVE-2026-40020-1.patch [new file with mode: 0644]
patches/CVE-2026-40020-2.patch [new file with mode: 0644]
patches/CVE-2026-40020-3.patch [new file with mode: 0644]
patches/CVE-2026-42006.patch [new file with mode: 0644]
patches/Use-_FORTIFY_SOURCE-level-3.patch [new file with mode: 0644]
patches/acl-Fix-crash-when-group-ACLs-are-used-but-user-s-ac.patch [new file with mode: 0644]
patches/auth__Terminate_properly_auth_oauth2_post_setting_defines.patch [new file with mode: 0644]
patches/auth__Use_AUTH_CACHE_KEY_USER_instead_of_per-database.patch [new file with mode: 0644]
patches/bug1104549-gssapi-regression.patch [new file with mode: 0644]
patches/bug1106784_Fix-LDAP-SASL-auth-support.patch [new file with mode: 0644]
patches/bug1121000_dovecot-ldap_Crash_if_iterate_filter_is_set_but_iterate_fields_is_not_set.patch [new file with mode: 0644]
patches/fit-32-bit-test-integers.patch [new file with mode: 0644]
patches/fix-man-errors.patch [new file with mode: 0644]
patches/lib-mail-istream-header-filter-Fix-potential-assert-.patch [new file with mode: 0644]
patches/lib-sieve_sieve-script_c_sieve_script_create_common_Correctly_handle_errors.patch [new file with mode: 0644]
patches/series [new file with mode: 0644]
patches/skip-rfc-subdir.patch [new file with mode: 0644]
po/nl.po [new file with mode: 0644]
rules [new file with mode: 0755]
salsa-ci.yml [new file with mode: 0644]
source/format [new file with mode: 0644]
source/lintian-overrides [new file with mode: 0644]
source_dovecot.py [new file with mode: 0644]
tests/control [new file with mode: 0644]
tests/doveadm [new file with mode: 0755]
tests/systemd [new file with mode: 0755]
tests/testmails [new file with mode: 0755]
tests/usage/00_setup [new file with mode: 0755]
tests/usage/imap [new file with mode: 0755]
tests/usage/pop3 [new file with mode: 0755]
tests/usage/sieve [new file with mode: 0755]
upstream/metadata [new file with mode: 0644]
upstream/signing-key.asc [new file with mode: 0644]
watch [new file with mode: 0644]

diff --git a/autogen.sh b/autogen.sh
new file mode 100755 (executable)
index 0000000..ba3a3cb
--- /dev/null
@@ -0,0 +1,6 @@
+#!/bin/sh
+
+set -eu
+
+autoreconf -f -i -v -Wall
+(cd pigeonhole && autoreconf -f -i -v -Wall)
diff --git a/changelog b/changelog
new file mode 100644 (file)
index 0000000..84b35de
--- /dev/null
+++ b/changelog
@@ -0,0 +1,3143 @@
+dovecot (1:2.4.1+dfsg1-6+deb13u7) trixie-security; urgency=medium
+
+  * Import upstream fixes for multiple security issues. (Closes: #1146018)
+    - CVE-2026-27852: DoS by sending mail with bad header
+    - CVE-2026-33263: submission-login: Panic when
+      mail_max_userip_connections is reached: Panic: epoll_ctl(del, 8)
+      failed: Bad file descriptor
+    - CVE-2026-33604: SMTP Smuggling via Missing Dot-Stuffing After Bare
+      Carriage Return
+    - CVE-2026-33605: managesieve-login: Pre-auth crash
+    - CVE-2026-33607: Dovecot IMAP LIST match_sub() Exponential
+      Backtracking â€” CPU Denial of Service
+    - CVE-2026-40013: pigeonhole: Stack Buffer Underflow in Pigeonhole
+      ManageSieve CHECKSCRIPT/PUTSCRIPT
+    - CVE-2026-40014: IMAP THREAD REFERENCES O(N²) CPU DoS via Crafted
+      References Header (index-thread-links.c)
+    - CVE-2026-40015: imap-hibernate can be crashed
+    - CVE-2026-40017: IMAP THREAD O(M³) CPU DoS via CRC32 Hash Collision
+      in strmap (mail-index-strmap.c / hash2.c)
+    - CVE-2026-40018: MySQL multi-byte escaping wrong
+    - CVE-2026-40204: acl: lda_mailbox_autocreate can bypass acl
+      restrictions
+    - CVE-2026-42007: Sieve editheader RCE
+    - CVE-2026-42391: imap: Pre-login memory/CPU growth with ID command
+    - CVE-2026-42394: sieve: symlink traversal flaw could result in
+      arbitrary file disclosure
+    - CVE-2026-52681: Sieve resource usage tracking lost when active
+      script changes
+    - CVE-2026-52687: IMAP: COMPRESS ZSTD can cause excessive memory usage
+    - CVE-2026-73209: imap-login crash: Self-recursion on zero-output
+      decompress chunks
+    - CVE-2026-33606: dsync: Mail content can cause dsync protocol
+      injection
+    - CVE-2026-40203: IMAP Compression Can Reveal Whether a Small Synced
+      Email Body Matches Sender-Chosen Text
+    - CVE-2026-42008: XCLIENT FORWARD= bare token not namespaced, allows
+      nopassword injection via trusted proxy
+    - CVE-2026-42392: imap-urlauth leaks memory into user-visible error
+      messages
+    - CVE-2026-42393: doveadm_password or api key length can still be
+      leaked with timing comparisons
+    - CVE-2026-42395: Single NUL-Byte XCLIENT FORWARD Payload Crashes
+    - CVE-2026-40205: OAuth2 passdb scope enforcement bypass via OR
+      semantics in remote validation path
+    - CVE-2026-73208: auth: db-oauth2: aud claim used as fallback for
+      missing scope claim
+  * lib-mail: istream-header-filter - Fix potential assert-crash
+    (Closes: #1144639)
+  * CI: Disable test-build-twice job, which is known to fail on trixie
+
+ -- Noah Meyerhans <noahm@debian.org>  Wed, 16 Sep 2026 15:06:35 -0400
+
+dovecot (1:2.4.1+dfsg1-6+deb13u6) trixie-security; urgency=medium
+
+  * Security update (Closes: #1136444)
+  * [76ceed4] CVE-2026-27851: lib-var-expand: Reset safe state when
+    transfer is unset
+  * [4af6fb3] CVE-2026-40016: lib-sieve: Enforce CPU time limit within
+    :contains and :matches matcher loops
+  * [366ef61] CVE-2026-33603: login-common: Only accept base64 in sasl
+  * [26bd41e] CVE-2026-40020: IMAP folders can be shared-spammed to
+    everyone.
+  * [b6f5bac] CVE-2026-42006: imap-login: Excessive memory usage DoS
+
+ -- Noah Meyerhans <noahm@debian.org>  Mon, 18 May 2026 16:03:51 -0400
+
+dovecot (1:2.4.1+dfsg1-6+deb13u5) trixie; urgency=medium
+
+  * [b357180] autopkgtests: Add managesieved authentication test
+  * [c9d69a1] Fix memory leak in CVE-2026-27857 fix
+
+ -- Noah Meyerhans <noahm@debian.org>  Wed, 06 May 2026 15:18:43 -0400
+
+dovecot (1:2.4.1+dfsg1-6+deb13u4) trixie-security; urgency=medium
+
+  * [bc29057] CVE-2025-59028: auth: Don't disconnect auth client when
+    invalid base64 SASL input is received
+  * [fee7a9a] CVE-2025-59031: stop shipping the decode2text shell script
+  * [9a4442e] CVE-2025-59032: managesieve-login: Fix crash when command
+    didn't finish on the first call
+  * [2711b3e] CVE-2026-24031, CVE-2026-27860: auth: fix ldap and sql
+    injection
+  * [d30f1c3] CVE-2026-27855: fix OTP authentication reply vulnerability
+  * [e1b0ff7] CVE-2026-27856: doveadm: fix timing oracle attack
+  * [b8a69bf] CVE-2026-27857: fix resource exhaustion DoS in NOOP command
+    parsing
+  * [85dd068] CVE-2026-27858: fix pre-authentication managesieve memory
+    consumption issue
+  * [880e332] CVE-2026-27859: fix uncontrolled resource allocation when
+    delivering specially crafted email messages
+
+ -- Noah Meyerhans <noahm@debian.org>  Tue, 31 Mar 2026 15:07:17 -0400
+
+dovecot (1:2.4.1+dfsg1-6+deb13u3) trixie; urgency=medium
+
+  * [1186296] import upstream fix for possible crash in ldap userdb
+    (Closes: #1121000)
+  * [14c13a5] backport upstream fix for crash in trash plugin (Closes: #1127029)
+  * [95bf3b3] Backport fix for segfault when group ACLs are present but the
+    user has no groups (Closes: #1129952)
+
+ -- Noah Meyerhans <noahm@debian.org>  Fri, 06 Mar 2026 09:36:28 -0500
+
+dovecot (1:2.4.1+dfsg1-6+deb13u2) trixie; urgency=medium
+
+  * [6ac2883] Clean up a few typos in default/example config (Closes: #1112667)
+  * [7feb544] Ensure default lmtpd auth_username_format matches the global value
+    (Closes: #1111469)
+  * [216ec20] import upstream patch for improperly terminated auth_oauth2_post_setting_defines
+    (Closes: #1116328)
+  * [46eab61] lib-sieve/sieve-script.c: sieve_script_create_common: Correctly handle errors.
+    (Closes: #1116070)
+
+ -- Noah Meyerhans <noahm@debian.org>  Thu, 23 Oct 2025 10:01:33 -0400
+
+dovecot (1:2.4.1+dfsg1-6+deb13u1) trixie-security; urgency=high
+
+  * Import upstream fix for an issue with authentication cache management that
+    could result in users being logged in as the wrong user in certain
+    configurations. (CVE-2025-30189) (Closes: #1115964)
+
+ -- Noah Meyerhans <noahm@debian.org>  Wed, 24 Sep 2025 09:14:50 -0400
+
+dovecot (1:2.4.1+dfsg1-6) unstable; urgency=medium
+
+  * [8c6ba88] Fix LDAP SASL auth support (Closes: #1106784)
+  * [fac9131] Avoid -fstack-clash-protection on hppa architecture
+    (Closes: #1107609)
+  * [071beb5] d/control: update Uploaders to reflect the current reality
+
+ -- Noah Meyerhans <noahm@debian.org>  Wed, 18 Jun 2025 10:01:58 -0400
+
+dovecot (1:2.4.1+dfsg1-5) unstable; urgency=medium
+
+  * [e6e5ef7] Fix typo in conf.d/auth-passwdfile.conf.ext (Closes: #1106072)
+  * [2a34ed0] Import upstream fix for dovecot-lda data loss (Closes: #1106485)
+  * [6c287ea] Fix typo in doveadm-fetch(1) (Closes: #1106533)
+  * [11f8b62] Fix invalid metadata in debian/patches/skip-rfc-subdir.patch
+
+ -- Noah Meyerhans <noahm@debian.org>  Wed, 28 May 2025 15:45:52 -0400
+
+dovecot (1:2.4.1+dfsg1-4) unstable; urgency=medium
+
+  * [235de56] managesieved: lintian: suppress library-not-linked-against-libc
+  * [74e7dcd] Ensure we use a 32-bit time_t on i386
+  * [461bc68] dovecot-core: postinst: fix purge of obsolete ucf conf files
+    (Closes: #1105003)
+
+ -- Noah Meyerhans <noahm@debian.org>  Mon, 12 May 2025 20:36:26 -0400
+
+dovecot (1:2.4.1+dfsg1-3) unstable; urgency=medium
+
+  * [bdf895a] upstream fix for GSSAPI authentication regression
+    (Closes: #1104549)
+  * [8f15017] d/copyright: Correct canonical reference to GNU licenses
+  * [ad8f661] dovecot-flatcurve: correct missing dependency on ucf
+  * [131567a] Fix miscellaneous groff errors in upstream manpages
+  * [a4a1404] Correct lintian warning debian-news-entry-has-unknown-version
+
+ -- Noah Meyerhans <noahm@debian.org>  Fri, 02 May 2025 11:20:05 -0400
+
+dovecot (1:2.4.1+dfsg1-2) unstable; urgency=medium
+
+  * [7c1b5b9] dovecot-ldap: postinst: correct conffile list (Closes: #1104047)
+  * [73a254a] config: set mail_home and add mbox deprecation note
+
+ -- Noah Meyerhans <noahm@debian.org>  Mon, 28 Apr 2025 15:53:54 -0400
+
+dovecot (1:2.4.1+dfsg1-1) unstable; urgency=medium
+
+  * Release 1:2.4.1+dfsg1-1 to unstable
+
+ -- Noah Meyerhans <noahm@debian.org>  Thu, 24 Apr 2025 13:06:17 -0400
+
+dovecot (1:2.4.1+dfsg1-1~exp2) experimental; urgency=medium
+
+  * [e30a611] flatcurve: Set breaks+replaces relationship on dovecot-fts-flatcurve
+    (Closes: #1102937)
+
+ -- Noah Meyerhans <noahm@debian.org>  Fri, 18 Apr 2025 15:34:42 -0400
+
+dovecot (1:2.4.1+dfsg1-1~exp1) experimental; urgency=medium
+
+  * [18c4170] d/watch: handle suffixes in upstream release filenames
+  * [340250d] New upstream version 2.4.1+dfsg1
+  * [2fcd382] Ensure we don't try to download files from the internet during build
+  * [4497016] refresh patches
+  * [af108c1] Remove obsolete patches
+  * [0ed2624] Fix 32-bit integer rollover in tests
+  * [528f075] d/rules: remove autogenerated sources on clean
+
+ -- Noah Meyerhans <noahm@debian.org>  Sun, 30 Mar 2025 11:48:57 -0400
+
+dovecot (1:2.4.0+dfsg1-1~exp6) experimental; urgency=medium
+
+  * [827f8b7] fix and re-enable 32-bit builds
+
+ -- Noah Meyerhans <noahm@debian.org>  Mon, 24 Mar 2025 15:16:57 -0400
+
+dovecot (1:2.4.0+dfsg1-1~exp5) experimental; urgency=medium
+
+  * [cea2f60] d/rules: remove additional files on clean
+  * [24e625b] ci: extend the timeout for the test-build-twice job
+  * [1df7d66] Fix ABI identification and add safety checking for it
+
+ -- Noah Meyerhans <noahm@debian.org>  Mon, 17 Mar 2025 09:56:33 -0400
+
+dovecot (1:2.4.0+dfsg1-1~exp4) experimental; urgency=medium
+
+  * [0806d3b] Move 90-sieve-extprograms back to dovecot-sieve where it belongs
+    (Closes: #1100146)
+
+ -- Noah Meyerhans <noahm@debian.org>  Thu, 13 Mar 2025 17:41:17 -0400
+
+dovecot (1:2.4.0+dfsg1-1~exp3) experimental; urgency=medium
+
+  * [4989934] Install a config file for flatcurve
+  * [baeb072] Add default lmtpd configuration
+  * [1d1d874] d/rules: drop some obsolete configure options
+  * [6bc7f0b] pigeonhole: enable ldap pluggin support
+  * [1998623] Enable experimental support for SMTPUTF8 and UTF8=ACCEPT
+  * [22abd27] enable cdb support
+  * [369d4a4] ci: disable i386 builds
+  * [e6ff65b] update autopkgtests
+  * [56dd0d6] debian/dovecot-core.NEWS: Document configuration changes
+  * [3d12c1e] d/rules: Update ABI detection
+
+ -- Noah Meyerhans <noahm@debian.org>  Tue, 11 Mar 2025 11:31:12 -0400
+
+dovecot (1:2.4.0+dfsg1-1~exp2) experimental; urgency=medium
+
+  * [ab636e4] Correct ucf handling of /etc/dovecot/dovecot.conf
+  * [ea5102c] Stop building for 32-bit architectures
+  * [88fecb4] fix shell error in dovecot-managesieved.postinst
+  * [68875fa] Fix some groff issues in manpages
+  * [b406a2f] drop stale lintian overrides
+  * [34e4a3b] lintian: ignore circular dependency between -core and -sieve
+  * [ee13475] d/copyright: Update or remove some attributions
+
+ -- Noah Meyerhans <noahm@debian.org>  Wed, 12 Feb 2025 16:39:17 -0500
+
+dovecot (1:2.4.0+dfsg1-1~exp1) experimental; urgency=medium
+
+  [ Michael Tokarev ]
+  * Update to 2.4.0
+
+  [ Noah Meyerhans ]
+  * Update default configuration from upstream example config bundle at
+    https://github.com/dovecot/tools/blob/main/dovecot-2.4.0-example-config.tar.gz
+  * Remove obsolete config files on upgrade if unmodified
+  * Update /usr/share/dovecot/protocols.d/* for 2.4
+
+ -- Noah Meyerhans <noahm@debian.org>  Tue, 11 Feb 2025 12:03:31 -0500
+
+dovecot (1:2.3.21.1+dfsg1-1) unstable; urgency=medium
+
+  [ Noah Meyerhans ]
+  * [452a10b] Move systemd unit files to /usr (Closes: #1071915)
+
+  [ Niels Thykier ]
+  * [a9caf51] Avoid unnecessary implicit requirement for `(fake)root`
+
+  [ Christian Göttsche ]
+  * [8c253d1] salsa-ci: enable build_twice job
+  * [47122cd] Bump to standards version 4.7.0 (no further changes)
+  * [4062094] Replace obsolete build-dependency pkg-config with pkgconf
+  * [f1221b8] Split overlong line in changelog
+  * [dd876aa] Annotate Debian patches
+  * [590287e] Fix typos in changelog
+
+  [ Noah Meyerhans ]
+  * [a212eb8] New upstream version 2.3.21.1+dfsg1
+    - Fix CVE-2024-23184 (Closes: #1078876)
+    - Fix CVE-2024-23185 (Closes: #1078877)
+
+ -- Noah Meyerhans <noahm@debian.org>  Sat, 17 Aug 2024 13:26:24 -0400
+
+dovecot (1:2.3.21+dfsg1-3) unstable; urgency=medium
+
+  * [883dc1a] Add libtirpc-dev to build-depends (Closes: #1065213)
+
+ -- Noah Meyerhans <noahm@debian.org>  Sat, 09 Mar 2024 22:31:22 -0800
+
+dovecot (1:2.3.21+dfsg1-2) unstable; urgency=medium
+
+  [ Christian Göttsche ]
+  * [a2fbc2f] split-protocols.patch: patch all-settings.c to successfully build
+    twice (Closes: #1044797)
+
+  [ Noah Meyerhans ]
+  * [70e4426] Drop arm64 from libunwind builddep arch list
+
+ -- Noah Meyerhans <noahm@debian.org>  Mon, 30 Oct 2023 13:40:35 -0700
+
+dovecot (1:2.3.21+dfsg1-1) unstable; urgency=medium
+
+  [ Noah Meyerhans ]
+  * [753b4fe] Don't build the unmaintained lucene fts plugin (Closes: #1040884)
+  * [5597486] New upstream version 2.3.21+dfsg1
+
+  [ Christian Göttsche ]
+  * [b8017f1] Cleanup temporary build files
+  * [35e1afe] Silence prototype conflicts
+  * [8dda8b9] Update Lintian overrides
+  * [6bae82f] Bump to standards version 4.6.2 (no further changes)
+  * [1f973d2] Mark hurd patch forwarded
+
+ -- Noah Meyerhans <noahm@debian.org>  Sat, 14 Oct 2023 08:52:10 -0700
+
+dovecot (1:2.3.20+dfsg1-1) unstable; urgency=medium
+
+  [ Christian Göttsche ]
+  * [fb2a5b7] d/rules: enable stack clash protection
+  * [2666970] d/patches: bump _FORTIFY_SOURCE to level 3
+
+  [ Noah Meyerhans ]
+  * [eab5171] New upstream version 2.3.20+dfsg1
+  * [d6135a4] Drop dependency on obsolete lsb-base package
+
+ -- Noah Meyerhans <noahm@debian.org>  Sun, 25 Jun 2023 16:17:56 -0700
+
+dovecot (1:2.3.19.1+dfsg1-2.1) unstable; urgency=medium
+
+  * Non-maintainer upload.
+  * [b02ebc9] Don't use deprecated crypt module.
+    (closes: #1028513)
+
+ -- Bas Couwenberg <sebastic@debian.org>  Fri, 20 Jan 2023 07:01:26 +0100
+
+dovecot (1:2.3.19.1+dfsg1-2) unstable; urgency=medium
+
+  [ Christian Göttsche ]
+  * [281fb2c] d/patches: cherry-pick fix for CVE-2022-30550 (Closes: #1016351)
+  * [9c58e71] d/patches: fix uninitialized read in doveadm-oldstats
+  * [a76a24d] d/control: bump to standards version 4.6.1 (no further changes)
+  * [4aaaa8b] Update Lintian overrides
+
+ -- Noah Meyerhans <noahm@debian.org>  Fri, 29 Jul 2022 19:58:28 -0700
+
+dovecot (1:2.3.19.1+dfsg1-1) unstable; urgency=medium
+
+  [ Christian Göttsche ]
+  * [e40f93f] d/patches: avoid usage of PATH_MAX not available on hurd
+  * [19e00cd] d/rules: enable backtrace generation
+  * [5bf1c43] d/patches: debug flaky unit test
+
+  [ Noah Meyerhans ]
+  * [b73422f] New upstream version 2.3.19.1+dfsg1
+  * [c88bfc0] Update changelog for 1:2.3.19.1+dfsg1-1 release
+  * [ca59548] Update lintian overrides
+  * [d6406c2] d/copyright: update declarations for current maintainers
+
+ -- Noah Meyerhans <noahm@debian.org>  Wed, 22 Jun 2022 09:27:01 -0700
+
+dovecot (1:2.3.19+dfsg1-1) unstable; urgency=medium
+
+  [ Christian Göttsche ]
+  * [0d29e45] d/rules: enable LTO via DEB_BUILD_MAINT_OPTIONS instead of custom flags
+  * [560cceb] d/source/lintian-overrides: update very-long-line-length-in-source-file overrides
+  * [b99d09e] d/copyright: update years
+  * [9ee8271] d/dovecot-core.prerm: drop as superseded by debhelper
+  * [907f85c] d/maintscripts: update
+  * [2b38240] d/dovecot-core.postinst: drop support for version skips
+  * [dcb76d1] d/dovecot-core.postinst: only link certs if existent (Closes: #1009872)
+  * [d223bbd] d/patches: add patch to support openssl 3.0 (Closes: #996273)
+
+  [ Noah Meyerhans ]
+  * [9f3175e] New upstream version 2.3.19+dfsg1
+
+ -- Noah Meyerhans <noahm@debian.org>  Sun, 05 Jun 2022 18:29:18 +0000
+
+dovecot (1:2.3.18+dfsg1-1) unstable; urgency=medium
+
+  [ Noah Meyerhans ]
+  * [36966c8] New upstream version 2.3.18+dfsg1
+  * [042bda4] Refresh patches for 1:2.3.18+dfsg1-1
+
+ -- "Noah Meyerhans" <noahm@debian.org>  Thu, 10 Feb 2022 20:05:50 +0000
+
+dovecot (1:2.3.17.1+dfsg1-1) unstable; urgency=medium
+
+  [ Christian Göttsche ]
+  * [40b0010] New upstream version 2.3.17+dfsg1
+  * [3c377e0] New upstream version 2.3.17.1+dfsg1
+  * [e2f1ce2] d/patches: rebase and drop upstream applied ones
+  * [533b7ad] d/control: bump to standards version 4.6.0 (no further changes)
+  * [02ed6cf] debian: reduce Lintian issues
+  * [bb3ae48] d/salsa-ci.yml: skip cross build and do not fail on Lintian
+    warnings
+  * [bcda7e4] d/control: build against Lua 5.4
+  * [9eed0dd] d/control: enable libunwind support on available archs
+  * [1990699] d/patches: cherry-pick memory leak commit
+  * [426df46] d/patches: cherry-pick imapsieve fix
+  * [e3d0747] d/patches: add patch for LTO by avoiding unaligned access
+    (Closes: #997513)
+
+ -- Noah Meyerhans <noahm@debian.org>  Tue, 14 Dec 2021 09:24:23 -0800
+
+dovecot (1:2.3.16+dfsg1-3) unstable; urgency=medium
+
+  * [7b858b6] Fix FTBFS on mips(64)el.  Stacktrace generation on these
+    architectures requires -funwind-tables, as with 32-bit arm.
+
+ -- Noah Meyerhans <noahm@debian.org>  Thu, 16 Sep 2021 08:41:27 -0700
+
+dovecot (1:2.3.16+dfsg1-2) unstable; urgency=medium
+
+  [ Christian Göttsche ]
+  * [e1e9ece] d/patches: rework backtrace test patch
+  * [be404bf] d/patches: add big-endian patch
+
+ -- Noah Meyerhans <noahm@debian.org>  Fri, 10 Sep 2021 16:10:50 -0700
+
+dovecot (1:2.3.16+dfsg1-1) unstable; urgency=medium
+
+  [ Christian Göttsche ]
+  * [ff4a227] New upstream version 2.3.14+dfsg1
+  * [963fa3b] New upstream version 2.3.15+dfsg1 (Closes: #991323, #983510)
+  * [5e0c898] d/watch: adjust dversionmangle for dfsg suffix
+  * [9ffb0f5] d/patches: update
+  * [850e1d6] New upstream version 2.3.16+dfsg1
+  * [7140b87] d/patches: rebase patches
+  * [fb1b77e] d/rules: enable LTO
+  * [ce7055d] d/control: add libsystemd-dev dependency
+  * [db93263] d/copyright: drop unused section
+  * [aeec1e8] d/rules: update how to set systemdsystemunitdir
+  * [ebe9709] d/patches: resolve compiler warnings
+  * [19b2bb0] d/changelog: bump to 1:2.3.16+dfsg1-1
+  * [58a4078] d/patches: update 32bit warnings patch
+
+  [ Noah Meyerhans ]
+  * [f217c2e] Fix indexer crash
+  * [b075317] Import upstream patch for indexer crash on client disconnect
+  * [36e8740] drop debian/dovecot-core.maintscript
+
+ -- Noah Meyerhans <noahm@debian.org>  Thu, 02 Sep 2021 13:22:16 -0700
+
+dovecot (1:2.3.13+dfsg1-2) unstable; urgency=high
+
+  * Import upstream fixes for security issues (Closes: #990566):
+    - CVE-2021-29157: Path traversal issue allowing an attacker with
+      access to the local filesystem can trick OAuth2 authentication into
+      using an HS256 validation key from an attacker-controlled location
+    - CVE-2021-33515: Sensitive information could be redirected to an
+      attacker-controlled address because of a STARTTLS command injection
+      bug in the submission service
+
+ -- Noah Meyerhans <noahm@debian.org>  Tue, 20 Jul 2021 08:05:19 -0700
+
+dovecot (1:2.3.13+dfsg1-1) unstable; urgency=medium
+
+  [ Christian Göttsche ]
+  * [6829237] New upstream version 2.3.13 (Closes: #979363)
+    - CVE-2020-24386: IMAP hibernation allows accessing other peoples mail
+    - CVE-2020-25275: MIME parsing crashes with particular messages
+
+  * [6d25736] Add libzstd-dev to build-dependencies (Closes: #969165)
+  * [5956798] Rebase patches
+  * [2cb63c3] Bump to standards version 4.5.1 (no further changes)
+  * [548bac5] Drop unmatched copyright src/lib-ntlm/* wildcard
+  * [6f33f3f] Ignore package-contains-documentation-outside-usr-share-doc
+    false-positives
+  * [dde9c94] Handle removed configuration file in postinst
+
+  [ Pino Toscano ]
+  * [04a60e3] d/{control,rules}: disable apparmor support on !linux archs
+    (Closes: #951869)
+
+  [ Helmut Grohne ]
+  * [e5f9fcb] d/patches: improve cross-compile support (Closes: #979370)
+
+ -- Noah Meyerhans <noahm@debian.org>  Mon, 25 Jan 2021 15:38:17 -0800
+
+dovecot (1:2.3.11.3+dfsg1-2) unstable; urgency=medium
+
+  [ Christian Göttsche ]
+  * [44770f6] Add patch for 32bit compiler warnings
+  * [053865a] Lintian: remove unused override
+  * [4ece2e1] Lintian: add forwarded header to Debian specific patches
+  * [67872b7] Lintian: ignore Debian only man page
+  * [d30bd7e] Lintian: tag manpage-without-executable got renamed to
+    spare-manual-page
+  * [3bdf952] Limit libcap-dev build-dependency to linux-any
+  * [28f6425] Drop acute accent in man page
+  * [8c15850] Add patch allowing GSSAPI containing NULL
+
+ -- Noah Meyerhans <noahm@debian.org>  Wed, 19 Aug 2020 12:06:07 -0700
+
+dovecot (1:2.3.11.3+dfsg1-1) unstable; urgency=high
+
+  * New upstream release fixes security issues (Closes: #968302)
+    - CVE-2020-12100 - Receiving mail with deeply nested MIME parts leads to
+      resource exhaustion as Dovecot attempts to parse it.
+    - CVE-2020-12673 - Dovecot's NTLM implementation does not correctly check
+      message buffer size, which leads to reading past allocation which can
+      lead to crash.
+    - CVE-2020-12674 - Dovecot's RPA mechanism implementation accepts
+      zero-length message, which leads to assert-crash later on.
+  * Add libcap-dev to build-dependencies to support dropping linux
+    capabilities.
+
+ -- Noah Meyerhans <noahm@debian.org>  Thu, 13 Aug 2020 16:21:24 -0700
+
+dovecot (1:2.3.10.1+dfsg1-2) unstable; urgency=medium
+
+  * Support sd_notify with systemd (Closes: #951722)
+  * Add necessary CFLAGS and LDFLAGS settings to ensure functional backtrace
+    generation.  (Closes: #962630)
+  * Suppress additional library-not-linked-against-libc lintian warnings some
+    plugins as false-positives, observed on armel systems
+
+  [ Andreas Hasenack ]
+  * d/t/control, d/t/testmails: cherry-pick updated autopkgtests from
+    Ubuntu's 1:2.2.35-2ubuntu1:
+    - d/t/testmails: dropped the hardcoded "Ubuntu" name from the banner
+      text and made it distribution agnostic
+    - d/t/control: added lsb-release to test dependencies, used to get the
+      distribution name
+
+ -- Noah Meyerhans <noahm@debian.org>  Tue, 16 Jun 2020 08:29:02 -0700
+
+dovecot (1:2.3.10.1+dfsg1-1) unstable; urgency=medium
+
+  * New upstream release addresses multiple security issues
+    - CVE-2020-10957
+    - CVE-2020-10958
+    - CVE-2020-10967
+    (Closes: #960963, #930919, #928492)
+  * Refresh patches
+  * Strip non-DFSG-compliant docs from .orig archives
+  * Incorporate a number of improvements to debian/ metadata contributed by
+    Christian Göttsche <cgzones@googlemail.com>
+  * Move pid file to /run (Closes: #925443)
+  * Add noahm@debian.org to Uploaders
+  * Work around flakiness in autopkgtest suite
+  * Suppress library-not-linked-against-libc lintian warnings some plugins as
+    false-positives
+
+ -- Noah Meyerhans <noahm@debian.org>  Wed, 10 Jun 2020 10:41:37 -0700
+
+dovecot (1:2.3.7.2-1) unstable; urgency=medium
+
+  * [dcaf24e] New upstream version 2.3.7.2
+    - Fixes CVE-2019-11500 for dovecot-core
+  * [111beef] Update pigeonhole to 0.5.7.2
+    - Fixes CVE-2019-11500 for pigeonhole/managesieve
+  * [a422c4c] Bump Standards-Version to 4.4.0; no changes needed
+  * [56e37ed] Bump dh compat to 12; no changes needed.
+    - Drop d/compat in favor debhelper-compat B-D.
+  * [476edbd] Refresh dovecot_name.patch and ssl-cert-location.patch
+  * [9dc7904] Drop patches included in 2.3.7.2.
+     - CVE-2019-10691
+     - CVE-2019-11494
+     - CVE-2019-11499
+     - CVE-2019-7524
+     - avoid-double-closing-mysql.patch
+     - lib-master-test-event-stats-Use-PRIu64-format.patch
+
+ -- Apollon Oikonomopoulos <apoikos@debian.org>  Thu, 29 Aug 2019 11:55:51 +0300
+
+dovecot (1:2.3.4.1-5) unstable; urgency=medium
+
+  * [bd00402] Fix CVE-2019-11494 and CVE-2019-11499 (Closes: #928235)
+     - submission-login: fix null pointer dereference when client
+       disconnects during authentication (CVE-2019-11494)
+     - submission-login: fix assert-crash when receiving an invalid
+       authentication message over TLS (CVE-2019-11499)
+
+ -- Apollon Oikonomopoulos <apoikos@debian.org>  Mon, 29 Apr 2019 23:35:05 +0300
+
+dovecot (1:2.3.4.1-4) unstable; urgency=high
+
+  * [d04d4ba] Fix assert-crash in JSON encoder (CVE-2019-10691)
+
+ -- Apollon Oikonomopoulos <apoikos@debian.org>  Thu, 18 Apr 2019 10:21:19 +0300
+
+dovecot (1:2.3.4.1-3) unstable; urgency=high
+
+  * [07c9212] Fix two buffer overflows when reading oversized FTS headers
+    and/or oversized POP3-UIDL headers (CVE-2019-7524).
+
+ -- Apollon Oikonomopoulos <apoikos@debian.org>  Mon, 25 Mar 2019 23:06:01 +0200
+
+dovecot (1:2.3.4.1-2) unstable; urgency=medium
+
+  [ Laurent Bigonville ]
+  * [ac99918] Fix double-free crash in mysql driver
+    Fix double closing of the connection in the mysql driver, this should
+    fix the crash in the dovecot auth process, taken from upstream.
+    (Closes: #918339)
+
+  [ Apollon Oikonomopoulos ]
+  * [8a30446] Bump Standards-Version to 4.3.0; no changes needed
+
+ -- Apollon Oikonomopoulos <apoikos@debian.org>  Thu, 14 Mar 2019 11:02:39 +0200
+
+dovecot (1:2.3.4.1-1) unstable; urgency=high
+
+  * [bebf0b4] New upstream version 2.3.4.1
+    + Fixes CVE-2019-3814: TLS client auth username handling
+
+ -- Apollon Oikonomopoulos <apoikos@debian.org>  Tue, 05 Feb 2019 16:19:12 +0200
+
+dovecot (1:2.3.4-2) unstable; urgency=medium
+
+  * [51d1317] Fix FTBFS on 32-bit platforms.
+    Cherry-pick upstream commit de42b54, fixing the event-stats test on
+    32-bit platforms.
+
+ -- Apollon Oikonomopoulos <apoikos@debian.org>  Sat, 24 Nov 2018 02:02:17 +0200
+
+dovecot (1:2.3.4-1) unstable; urgency=medium
+
+  * [14c247f] New upstream version 2.3.4
+  * [7fed004] Update pigeonhole to 0.5.4
+
+ -- Apollon Oikonomopoulos <apoikos@debian.org>  Fri, 23 Nov 2018 22:00:06 +0200
+
+dovecot (1:2.3.3-1) unstable; urgency=medium
+
+  [ Jelmer Vernooij ]
+  * Trim trailing whitespace.
+
+  [ Apollon Oikonomopoulos ]
+  * [6591a99] New upstream version 2.3.3
+  * [3d718ec] Bump Standards-Version to 4.2.1; no changes needed
+  * [123bd32] Update pigeonhole to 0.5.3
+
+ -- Apollon Oikonomopoulos <apoikos@debian.org>  Thu, 04 Oct 2018 17:29:40 +0300
+
+dovecot (1:2.3.2.1-1) unstable; urgency=medium
+
+  * [40ba9f0] New upstream bugfix release 2.3.2.1
+  * [87045ac] Drop fix-ftbfs-on-32bit.patch; merged upstream
+  * [5bb22a4] Bump Standards-Version to 4.1.5; no changes needed
+
+ -- Apollon Oikonomopoulos <apoikos@debian.org>  Tue, 10 Jul 2018 17:51:43 +0300
+
+dovecot (1:2.3.2-2) unstable; urgency=medium
+
+  * [48067de] Fix FTBFS on 32-bit platforms by cherry-picking upstream commit
+    1e23986f.
+
+ -- Apollon Oikonomopoulos <apoikos@debian.org>  Wed, 04 Jul 2018 12:43:14 +0300
+
+dovecot (1:2.3.2-1) unstable; urgency=medium
+
+  * [bb03669] New upstream version 2.3.2
+    + Upload to unstable
+  * [d29da3a] Merge 2.3 package changes from experimental. Important changes:
+    + [b3d1e17] Enable AppArmor support, see
+      https://wiki2.dovecot.org/Plugins/Apparmor
+      â€¢ B-D on libapparmor-dev
+    + [c0c55bd] Enable Lua scripting support for authdb/passdb.
+      â€¢ B-D on liblua5.3-dev
+      â€¢ New binary package, dovecot-auth-lua
+    + [4f6792e] Build with sodium support, enabling the ARGON2I and ARGON2ID
+      password schemes.
+      â€¢ B-D on libsodium-dev
+    + [54347e7] Build with ICU support enabling FTS unicode normalization
+      â€¢ B-D on libicu-dev
+    + New dovecot-submissiond binary package for the dovecot submission agent;
+      see https://wiki2.dovecot.org/Submission.
+  * [4db4813] Change maintainer address to dovecot@packages.d.o
+  * [5118354] Update pigeonhole to 0.5.2
+  * [52a7af4] Drop murmur3-big-endian.patch; merged upstream
+  * [22a6eee] Refresh dovecot_name.patch
+  * [3af7568] dovecot_name.patch: apply to submissiond as well
+
+ -- Apollon Oikonomopoulos <apoikos@debian.org>  Wed, 04 Jul 2018 08:57:45 +0300
+
+dovecot (1:2.2.36-1) unstable; urgency=medium
+
+  * [19f2274] d/gbp.conf: set merge-mode to "merge" to preserve pigeonhole/
+    when importing new dovecot sources
+  * [6b9bf0d] New upstream version 2.2.36
+  * [be12f22] Bump pigeonhole version to 0.4.24
+    + Remove new file under doc/rfc
+    + Ship the new imap_filter_sieve module in dovecot-sieve
+  * [b77be59] Bump Standards-Version to 4.1.4; no changes needed
+
+ -- Apollon Oikonomopoulos <apoikos@debian.org>  Wed, 06 Jun 2018 09:31:49 +0300
+
+dovecot (1:2.2.35-2) unstable; urgency=medium
+
+  * [7665652] Use git-subtree to generate pigeonhole patch from git; add
+    single-debian-patch to d/source/local-options
+  * [bfa0f10] d/rules: specify libdir manually; previous upload moved modules
+    under /usr/lib/<triplet>, which was bound to break existing setups
+  * [982e826] d/copyright: adjust pigeonhole path and bump years
+
+ -- Apollon Oikonomopoulos <apoikos@debian.org>  Thu, 22 Mar 2018 16:56:40 +0200
+
+dovecot (1:2.2.35-1) unstable; urgency=medium
+
+  * [8108cba] New upstream version 2.2.35
+  * [6cbbaa1] Update pigeonhole to 0.4.23 (Closes: #892137)
+  * [9ace5f2] Switch Vcs-* URLs to salsa.d.o
+  * [ef40625] d/rules: call configure via dh_auto_configure.
+    Thanks to Helmut Grohne (Closes: #885854)
+  * [a459455] Drop B-D on libcurl4-gnutls-dev; removed upstream since 2.2
+  * [235af9d] Update upstream signing key
+
+ -- Apollon Oikonomopoulos <apoikos@debian.org>  Tue, 20 Mar 2018 11:15:42 +0200
+
+dovecot (1:2.2.34-2) unstable; urgency=high
+
+  * [868dc65] Update pigeonhole to 0.4.22
+  * Set urgency to high due to the security fixes in 2.2.34-1
+
+ -- Apollon Oikonomopoulos <apoikos@debian.org>  Fri, 02 Mar 2018 18:36:23 +0200
+
+dovecot (1:2.2.34-1) unstable; urgency=medium
+
+  * [f53dc9a] New upstream version 2.2.34 (Closes: #921529)
+    Fixes the following security issues:
+     + CVE-2017-15130: TLS SNI config lookups may lead to excessive memory
+       usage (Closes: #891820)
+     + CVE-2017-14461: rfc822_parse_domain information leak vulnerability
+       (Closes: #891819)
+     + CVE-2017-15132: auth client leaks memory if SASL authentication is
+       aborted (Closes: #888432)
+  * [0dc98c6] Do not patch all-settings.c; regenerate it at build time
+    instead. Thanks to Aki Tuomi!
+  * [e678e3b] Bump dh compat to 11
+     + B-D on debhelper (>= 11~)
+     + Use dh_installsystemd instead of dh_systemd_enable
+  * [271b290] Bump Standards-Version to 4.1.3; no changes needed
+  * [3cd6715] d/copyright: bump upstream and debian years
+  * [380d1ac] Drop the ENABLED flag from /etc/default/dovecot (but let the
+    initscript handle it if it exists)
+  * [97d6fae] d/watch: switch upstream URL to https://
+
+ -- Apollon Oikonomopoulos <apoikos@debian.org>  Thu, 01 Mar 2018 10:55:49 +0200
+
+dovecot (1:2.2.33.2-1) unstable; urgency=medium
+
+  * [8216f38] New upstream version 2.2.33.2
+
+ -- Apollon Oikonomopoulos <apoikos@debian.org>  Sat, 11 Nov 2017 20:59:43 +0200
+
+dovecot (1:2.2.33.1-1) unstable; urgency=medium
+
+  * [dbd1132] New upstream version 2.2.33.1
+    + [b3d1f2d] Refresh split-protocols.patch
+    + [e0de123] Update pigeonhole to 0.4.21
+  * [ef6a1eb] Set mail_privileged_group to 'mail' by default (Closes: #711856)
+  * [aeb6cf3] d/copyright: convert to Format 1.0
+  * [5961f9d] Use dh-autoreconf for both, dovecot and pigeonhole.
+  * [85f1f0f] Bump Standards to 4.1.1; no changes needed
+
+ -- Apollon Oikonomopoulos <apoikos@debian.org>  Fri, 13 Oct 2017 16:28:14 +0300
+
+dovecot (1:2.2.32-2) unstable; urgency=medium
+
+  * [fa71c69] dovecot-core.postinst: remove dovecot-common's postrm
+    (Closes: #696382)
+  * [e835c67] Ship decode2text.sh as an example (Closes: #767313)
+  * [63fb486] Deprecate dovecot-dbg in favor of auto dbgsyms
+  * [36b44b9] Handle unsupported SSLv2/SSLv3 in the ssl_protocols setting
+    gracefully (Closes: #866752)
+
+ -- Apollon Oikonomopoulos <apoikos@debian.org>  Tue, 19 Sep 2017 16:59:38 +0300
+
+dovecot (1:2.2.32-1) unstable; urgency=medium
+
+  * [6652d9c] New upstream version 2.2.32.
+  * [c9cb096] Update pigeonhole to 0.4.20.
+  * [b499950] dovecot-core: remove SSL key/cert symlinks on purge
+    (Closes: #867157)
+  * [dbdcc66] dovecot-core.postinst: ignore adduser errors (Closes: #867849)
+  * [476c950] Bump Standards to 4.1.0; no changes needed.
+  * [2914efa] Drop B-D on autotools-dev, it is depended on by debhelper 10.
+  * [305d022] Remove Fabio, Joel and Marco from Uploaders. Thanks for your work!
+
+ -- Apollon Oikonomopoulos <apoikos@debian.org>  Tue, 12 Sep 2017 16:15:52 +0300
+
+dovecot (1:2.2.31-1) unstable; urgency=medium
+
+  * [9b058f3] New upstream version 2.2.31
+    + [2b577c1] Bump pigeonhole version to 0.4.19
+  * Enable TLS by default:
+    + [7ca4b1c] Update SSL cert location patch; cert/key should reside under
+      /etc/dovecot/private by default.
+    + [05d3d0f] Use ssl-cert-snakeoil certificates to setup SSL by default
+      (Closes: #376146, #786570)
+    + [862901f] dovecot-core.postinst: manage 10-ssl.conf using ucf
+      (Closes: #850538)
+    + [418df05] README.Debian: document the new TLS setup
+    + [47bade9] dovecot-core.NEWS: document TLS support
+  * [8356bc0] Handle /etc/dovecot/private mode using dpkg-statoverride
+  * dovecot-core.postinst: cleanup
+    + [afbd33f] dovecot-core.postinst: always call adduser
+    + [ee22dc5] dovecot-core.postinst: remove obsolete conffile handling
+    + [7bb298b] dovecot-core.postinst: do not remove the imapd user/group
+  * [815a2d1] README.Debian: cleanup
+  * [91115a3] Use noawait dpkg triggers (lintian warning)
+  * [e845cec] Add basic usage DEP-8 test, doing end-to-end tests involving
+    LDA, IMAP and POP3.
+  * [71c73ef] systemd: convert service to Type=simple and start after
+    network-online.target (Closes: #865546, #825562)
+  * [1534fac] dovecot.service: enable ProtectSystem=full
+  * [d276c69] B-D only on default-libmysqlclient-dev
+
+ -- Apollon Oikonomopoulos <apoikos@debian.org>  Tue, 27 Jun 2017 18:18:12 +0300
+
+dovecot (1:2.2.30.2-1) unstable; urgency=medium
+
+  * [401e83d] New upstream version 2.2.30.2
+  * [1ea8321] Bump pigeonhole version to 0.4.18
+  * [97bf8ec] Drop CVE-2017-2669 patch
+  * [9c1bbe2] Drop fix-sha3-on-big-endian.patch
+  * [d3c607b] Refresh dovecot_name.patch
+  * [5c64268] Bump Standards to 4.0.0; no changes needed
+  * [0b884fc] Bump compat to 10
+    + B-D on debhelper (>= 10)
+    + Drop B-D on dh-systemd, now provided by debhelper
+    + Run dh --without=autoreconf, since we use autotools-dev
+
+ -- Apollon Oikonomopoulos <apoikos@debian.org>  Thu, 22 Jun 2017 22:22:59 +0300
+
+dovecot (1:2.2.27-3) unstable; urgency=high
+
+  * [117285a] Remove /etc/dovecot/README (Closes: #849290)
+  * [04e8ce3] auth: Do not double-expand key in passdb dict when
+    authenticating (CVE-2017-2669) (Closes: #860049)
+
+ -- Apollon Oikonomopoulos <apoikos@debian.org>  Tue, 11 Apr 2017 00:46:54 +0300
+
+dovecot (1:2.2.27-2) unstable; urgency=medium
+
+  * [30586e3] Fix SHA3 on big-endian architectures.
+
+ -- Apollon Oikonomopoulos <apoikos@debian.org>  Thu, 15 Dec 2016 22:24:56 +0200
+
+dovecot (1:2.2.27-1) unstable; urgency=medium
+
+  [ Jaldhar H. Vyas ]
+  * [b1e4693] Imported Upstream version 2.2.27
+    + Includes fix for CVE-2016-8652 (Closes: #846605)
+
+  [ Apollon Oikonomopoulos ]
+  * [b25993a] Drop patches merged upstream:
+    + call_openssl_cleanup_at_deinit.patch
+    + disable_sslv23.patch
+
+ -- Apollon Oikonomopoulos <apoikos@debian.org>  Wed, 14 Dec 2016 21:48:46 +0200
+
+dovecot (1:2.2.26.0-4) unstable; urgency=medium
+
+  * [3015f35] Drop references to SSLv2 in the default SSL protocols (Closes: #844271)
+
+ -- Apollon Oikonomopoulos <apoikos@debian.org>  Mon, 14 Nov 2016 17:55:26 +0200
+
+dovecot (1:2.2.26.0-3) unstable; urgency=medium
+
+  * [b03027b] Call OPENSSL_cleanup() on dcrypt_openssl unload. Fixes FTBFS
+    with OpenSSL 1.1.0c.
+
+ -- Apollon Oikonomopoulos <apoikos@debian.org>  Sun, 13 Nov 2016 10:56:30 +0200
+
+dovecot (1:2.2.26.0-2) unstable; urgency=medium
+
+  * [9db7d1b] Fix upgrades from 2.2.25 (Closes: #843028)
+
+ -- Apollon Oikonomopoulos <apoikos@debian.org>  Tue, 08 Nov 2016 15:06:16 +0200
+
+dovecot (1:2.2.26.0-1) unstable; urgency=medium
+
+  [ Apollon Oikonomopoulos ]
+  * [18fc181] New upstream version 2.2.26.0 (Closes: #828286, #834837)
+  * [3ecfd3c] Update pigeonhole to 0.4.16
+  * [61ff825] Move libdovecot-ldap and libdict_ldap to dovecot-ldap (Closes:
+    #830135).
+  * [b3a1650] Ubuntu: disable -Bsymbolic-functions ld flag.
+    Thanks to Christian Ehrhardt <christian.ehrhardt@canonical.com>
+    (Closes: #842151) (LP: #1636781)
+  * [5828ab1] B-D on default-libmysqlclient-dev (but keep plain
+    libmysqlclient-dev as an alternative to ease backports).
+  * [0086110] Drop DRAC plugin.
+    Thanks to Christian Ehrhardt <christian.ehrhardt@canonical.com>
+    (Closes: #842153)
+
+  [ Jaldhar H. Vyas ]
+  * [60808eb] Move aclocal *.m4 files into -dev package.
+  * [52fd869] Move lib95_imap_sieve_plugin.so into dovecot-sieve (Closes:
+    #832046).
+
+ -- Apollon Oikonomopoulos <apoikos@debian.org>  Thu, 07 Jul 2016 10:17:58 +0200
+
+dovecot (1:2.2.25-1) unstable; urgency=medium
+
+  * [cc29a81] Imported Upstream version 2.2.25
+  * [d19bcca] Updated pigeonhole patch to 0.4.14
+  * [16db179] Merged in some features of the Ubuntu dovecot package.
+    + dovecot-core: added lsb-base dependency.
+    + dovecot-core: Added apport hook.
+    + dovecot-imapd,dovecot-pop3d: Added ufw profiles.
+    Thanks to Christian Erhardt <christian.ehrhardt@canonical.com>
+    (Closes: #828864)
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Fri, 01 Jul 2016 17:07:03 -0400
+
+dovecot (1:2.2.24-1) unstable; urgency=medium
+
+  * [26020b6] Imported Upstream version 2.2.24 (Closes: #818652)
+
+ -- Apollon Oikonomopoulos <apoikos@debian.org>  Mon, 09 May 2016 10:42:08 +0300
+
+dovecot (1:2.2.23-1) unstable; urgency=medium
+
+  [ Jaldhar H. Vyas ]
+  * Drop missing-expunges.patch, merged upstream
+
+  [ Apollon Oikonomopoulos ]
+  * [8a01915] Imported Upstream version 2.2.23
+
+ -- Apollon Oikonomopoulos <apoikos@debian.org>  Tue, 12 Apr 2016 17:30:03 +0300
+
+dovecot (1:2.2.22-1) unstable; urgency=medium
+
+  [ Jaldhar H. Vyas ]
+  * [2321581] Imported Upstream version 2.2.22
+  * [3fa8a62] Updated pigeonhole patch to 0.4.13
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Fri, 18 Mar 2016 19:18:34 -0400
+
+dovecot (1:2.2.21-1) unstable; urgency=medium
+
+  [ Jaldhar H. Vyas ]
+  * [d9c0630] Imported Upstream version 2.2.21 (Closes: #809666, #708539,
+    #801346, 803223)
+  * [5360548] Updated pigeonhole patch to 0.4.12
+  * [5e6783f] Fixed typo in dovecot-core.README.Debian.
+    Thanks to Ingo Wichmann <iw-debian@linuxhotel.de> (Closes: #809717)
+  * [d00d0c7] Create /var/lib/dovecot in the package. (Closes: #801752)
+  * [6510373] Upstream patch for sync problem which could cause expunged
+    messages to keep reappearing. (Closes: #684499)
+  * [040f7fa] dovecot-core: dovecot.socket not enabled on installation
+    (Closes: #814999)
+  * [fc29003] /etc/dovecot/10-ssl.conf no longer managed by ucf or modified by
+    postinst. Thanks to Santiago Vila <sanvila@unex.es> (Closes: #773237)
+  * [0d16e16] Fixed some lintian warnings.
+  * [801ba7e] Added Apollon to uploaders
+
+  [ Apollon Oikonomopoulos ]
+  * [99ef3d8] Build with lz4 support (Closes: #784321)
+  * [92f68aa] Fix nss userdb (Closes: #712764)
+  * [41b9bde] Disable dovecot.socket in existing installations.
+  * [49c5b97] d/rules: specify systemd unit dir manually (Closes: #720854)
+  * [a377ccb] Convert to dh sequencer
+  * [48af954] B-D on debhelper >= 9
+  * [591c315] Bump standards to 3.9.7; no changes needed
+  * [ceb629e] Use dh_installinit --name
+  * [ef8d8ac] d/rules: refactor file installation
+  * [a899bcc] dovecot-core: use dh_installman
+  * [6159e00] d/rules: build in parallel if requested
+  * [51014e6] d/control: use HTTPS Vcs-* URLs
+  * [b385cf5] dovecot-sieve: replace Conflicts with Breaks
+  * [b4a9e68] Add basic DEP-8 tests
+  * [7452649] Add DEP-8 tests for systemd support
+  * [e5101aa] Re-enable PIE and bindnow
+  * [5717808] Fix invoke-rc.d calls and never call init.d directly
+  * [3504241] Drop debconf remains
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Tue, 01 Mar 2016 19:31:42 -0500
+
+dovecot (1:2.2.20-1) UNRELEASED; urgency=medium
+
+  * [68d5038] Imported Upstream version 2.2.20
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Sun, 13 Dec 2015 10:21:21 -0500
+
+dovecot (1:2.2.19-1) UNRELEASED; urgency=medium
+
+  * [146ef57] Imported Upstream version 2.2.19
+  * [3af7cad] Updated pigeonhole patch to 0.4.9
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Sun, 13 Dec 2015 09:41:56 -0500
+
+dovecot (1:2.2.18-2) unstable; urgency=high
+
+  * [3f3bf71] Updated pigeonhole patch to 0.4.8 (Closes: #792669)
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Sun, 23 Aug 2015 23:16:28 -0400
+
+dovecot (1:2.2.18-1) unstable; urgency=medium
+
+  * [cce20a5] Imported Upstream version 2.2.18. Closes: #786760
+  * [36d2ec1] Refresh patch dovecot_name.patch.
+  * [109e6f8] Drop patch cve-2015-3420.patch: applied upstream.
+  * [6c59f09] Depend on krb5-multidev rather than libkrb5-dev.
+
+ -- Jelmer Vernooij <jelmer@debian.org>  Sun, 24 May 2015 15:01:19 +0000
+
+dovecot (1:2.2.16-1) unstable; urgency=medium
+
+  * [e9d9193] Imported Upstream version 2.2.16
+  * [976c256] Remove gbp- prefix from section names in debian/gbp.conf.
+  * [762b9a6] Add Dutch translation. Thanks, Frans Spiesschaert. Closes: #766203
+  * [dea3dd6] Drop bye_logout_not_sent.patch: already included upstream.
+
+ -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 May 2015 12:23:05 +0000
+
+dovecot (1:2.2.13-12) unstable; urgency=high
+
+  * [48f6fe4] Add patch cve-2015-3420.patch: Fix SSL/TLS handshake failures
+    leading to a crash of the login process with newer versions of OpenSSL.
+    Closes: #783649 (CVE-2015-3420)
+
+ -- Jelmer Vernooij <jelmer@debian.org>  Mon, 04 May 2015 11:38:30 +0000
+
+dovecot (1:2.2.13-11) unstable; urgency=high
+
+  * [ebc0377] Don't allow install of dovecot-sieve without a new enough
+    dovecot-core. (Closes: #772885)
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Sun, 14 Dec 2014 12:27:50 -0500
+
+dovecot (1:2.2.13-10) unstable; urgency=high
+
+  [ Jelmer Vernooij ]
+  * [93db7f0] Fix path to 90-sieve-extprograms.conf in dovecot-sieve.postinst.
+    Closes: #772703, #772711
+
+  [ Jaldhar H. Vyas ]
+  * [8c627a4] Add another db_stop this time for triggers.  Die #770695 die!
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Wed, 10 Dec 2014 19:56:20 -0500
+
+dovecot (1:2.2.13-9) unstable; urgency=high
+
+  * [5b689f6] Made some overlooked configuration files into conffiles;
+    deleted excess files from /usr/share/doc/dovecot-core.
+  * [83f2fc4] Explicitly stop debconf in dovecot-core postinst which hopefully
+    fixes the last of the install hangs.
+    Thanks to Chris Gilbert <zeke.gilbert@gmail.com> (Closes: #770695)
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Tue, 09 Dec 2014 06:17:23 -0500
+
+dovecot (1:2.2.13-8) unstable; urgency=medium
+
+  * [b2f652f] Turn off SSL by default and leave SSL cert locations commented
+    out. Hopefully this will be a satisfactory lowest common denominator for
+    new installs without messing with upgrades.
+    (Closes: #771334, #771407)
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Tue, 02 Dec 2014 00:21:30 -0500
+
+dovecot (1:2.2.13-7) unstable; urgency=high
+
+  * [daea09a] Commented out cert locations so install doesn't bomb if the
+    certs haven't been created yet.
+    (Closes: #706216, #732263, #767154, #768253, #769461, #770697)
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Fri, 28 Nov 2014 00:27:03 -0500
+
+dovecot (1:2.2.13-6) unstable; urgency=medium
+
+  * [f7205c0] dovecot-dev: removed dependency on dovecot-core
+  * [e393868] Removed SSL certificate generation from postinst.  From now on
+    you have to do this yourself.  See dovecot-core's README.debian for
+    instructions. (Closes: #730828)
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Sat, 25 Oct 2014 23:31:42 -0400
+
+dovecot (1:2.2.13-5) unstable; urgency=medium
+
+  * Fix name of organizationName field in SSL configuration for self-
+    signed certs. Closes: #760653
+
+ -- Jelmer Vernooij <jelmer@debian.org>  Sat, 06 Sep 2014 23:19:51 +0200
+
+dovecot (1:2.2.13-4) unstable; urgency=medium
+
+  * Add Provides with dovecot ABI version, for plugins to depend on.
+    Closes: #456021
+
+ -- Jelmer Vernooij <jelmer@debian.org>  Sun, 20 Jul 2014 19:31:09 +0200
+
+dovecot (1:2.2.13-3) unstable; urgency=medium
+
+  * Build-depend on clucene 2.3 or later, which upstream lists as the
+    minimum version. Closes: #754141
+  * Use configuration file for openssl certificate configuration.
+  * Use canonical address for Vcs-Browser header.
+  * Remove unnecessary asterisk from NEWS entry; fixes lintian warning.
+  * Remove unused lintian override for usr/lib/dovecot/imap.
+  * dovecot-core: Ignore lintian warnings for empty directories.
+
+ -- Jelmer Vernooij <jelmer@debian.org>  Tue, 08 Jul 2014 01:48:08 +0200
+
+dovecot (1:2.2.13-2) unstable; urgency=medium
+
+  * [bd5e34b] Patches from upstreams' hg repo to fix BYE and LOGOUT not being
+    sent. (Closes: #751682)
+  * [452e336] Czech translation for debconf.
+    Thanks to Michal Å imůnek (Closes: #751389)
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Wed, 25 Jun 2014 01:38:59 -0400
+
+dovecot (1:2.2.13-1) unstable; urgency=medium
+
+  * [0680040] Imported Upstream version 2.2.13
+
+ -- Jelmer Vernooij <jelmer@debian.org>  Sun, 25 May 2014 18:45:38 +0200
+
+dovecot (1:2.2.13~rc1-1) unstable; urgency=medium
+
+  * [7751508] Imported Upstream version 2.2.13~rc1
+    + Fixes denial of service vulnerability (CVE-2014-3430). Closes: #747549
+
+ -- Jelmer Vernooij <jelmer@debian.org>  Sat, 10 May 2014 14:34:14 +0200
+
+dovecot (1:2.2.12-3) unstable; urgency=medium
+
+  * [0383db0] Break long line.
+  * [c961b6a] Fix installation of sieve plugins. Closes: #742770, #684271
+  * [388d2bf] Strip body.rfc5173.txt from the pigeonhole patch, as it is
+    non-free. Closes: #745398
+
+ -- Jelmer Vernooij <jelmer@debian.org>  Wed, 23 Apr 2014 02:56:56 +0200
+
+dovecot (1:2.2.12-2) unstable; urgency=medium
+
+  * [c882a38] Add build dependencies libstemmer-dev and libexttextcat-dev,
+    used by dovecot-lucene.
+  * [67762d4] Use autotools-dev to update config.guess and config.sub.
+  * [33e79b7] Update Japenese po file. Thanks, victory. Closes: #730171
+  * [cbf213b] Add non-standard-dir-perm override for /etc/dovecot/private.
+
+ -- Jelmer Vernooij <jelmer@debian.org>  Sun, 23 Mar 2014 17:57:04 +0000
+
+dovecot (1:2.2.12-1) experimental; urgency=medium
+
+  * [52b67de] Update watch file for 2.2 series.
+  * [c4eac1f] Verify upstream tarball signature in watch file.
+  * [3a81ff9] Imported Upstream version 2.2.12
+  * [8950a86] Bump standards version to 3.9.5 (no changes).
+  * [cd82417] Add myself to uploaders.
+
+ -- Jelmer Vernooij <jelmer@debian.org>  Thu, 06 Mar 2014 22:45:19 +0000
+
+dovecot (1:2.2.10-1) unstable; urgency=low
+
+  * [0496c52] Imported Upstream version 2.2.10
+  * [515dd61] Added new package for Lucene full text search support.
+    Thanks to Jelmer Vernooij <jelmer@debian.org> for the patch.
+    (Closes: #685979)
+  * [718a68e] Fix old private key location in README.Debian.
+    Thanks to Jelmer Vernooij <jelmer@debian.org> for the patch.
+    (Closes: #702385)
+  * [42da568] dovecot-solr: Make sure solr-schema.xml gets installed.
+    (In /usr/share/dovecot)
+    (Closes: #695185)
+  * [77acbf7] Added /usr/share/dovecot/dovecot-abi file in dovecot-dev to
+    document -- you guessed it! -- the dovecot ABI version.
+  * [feb597a] Added support for xz compression.
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Thu, 06 Mar 2014 02:51:34 -0500
+
+dovecot (1:2.2.9-1) unstable; urgency=low
+
+  [ Jaldhar H. Vyas ]
+  * [77468cf] Imported Upstream version 2.2.9
+  * [43e08f3] Place dovenull user in its own group. (Closes: #725164)
+  * [e1a3e9c] Handled the fact that dovecot-db.conf.ext is no longer used.
+    (Closes: #728107, #730403)
+
+  [Debconf translation updates]
+  * Russian (Yuri Kozlov).  (Closes: #729106)
+  * German (Chris Leick).  (Closes: #729358)
+  * Danish (Joe Hansen).  (Closes: #729425)
+  * French (Julien Patriarca).  (Closes: #729966)
+  * Portuguese (Américo Monteiro).  (Closes: #730006)
+  * Polish (MichaÅ‚ KuÅ‚ach).  (Closes: #730061)
+  * Italian (Beatrice Torracca).  (Closes: #730136)
+  * Japanese (victory).  (Closes: #73017)
+  * Swedish (Martin Bagge / brother).  (Closes: #730188)
+  * Spanish; (Camaleón).  (Closes: #730354)
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Thu, 16 Jan 2014 15:42:13 -0500
+
+dovecot (1:2.2.8-1) UNRELEASED; urgency=low
+
+  * [6157a2b] New upstream version 2.2.8
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Thu, 21 Nov 2013 16:54:46 -0500
+
+dovecot (1:2.2.5-1) experimental; urgency=low
+  [ Micah Anderson ]
+  * [a0035bf] New upstream version 2.2.5
+  * [a053c49] Update pigeonhole patch to 0.4.1
+  * [689cd67] refreshed patches
+
+  [ Jaldhar H. Vyas ]
+  * Caused bugs and then fixed them again.
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Mon, 09 Sep 2013 00:57:32 -0400
+
+dovecot (1:2.1.17-2) unstable; urgency=low
+
+  * [e8286e0] New version of drac patch taken from Ubuntu which works better
+    with 2.x (Closes: #716764)
+  * [23acb40] Add a patch from Ubuntu to report the distro name in the login
+    banner why not.
+  * [f8d566e] Don't need dovecot-common package anymore; get rid of it.
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Sat, 07 Sep 2013 14:58:05 -0400
+
+dovecot (1:2.1.17-1) experimental; urgency=low
+
+  [ Jaldhar H. Vyas ]
+  * [fa0d6aa] Re-enable mbox write locking patch to comply with policy 11.6
+    (Closes: #720502)
+  * [38691fb] New upstream version (Closes: #719021)
+  * [1361144] prompts in dovecot-core postinst debconfiscated.
+  * IN MEMORIAM: Goldy the Goldfish (2000-2013)  You were a prince (or
+    perhaps princess?) among fish and we shall all miss you dearly.
+    May your karmas merit much punya in future lives.
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Fri, 23 Aug 2013 01:46:20 -0400
+
+dovecot (1:2.1.16-1) experimental; urgency=low
+
+  * [9741bd8] New Upstream version
+  * [3476489] Updated pigeonhole patch to 0.3.5
+  * [d4f236f] Removed some patches which are no longer required.
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Fri, 14 Jun 2013 16:25:19 -0400
+
+dovecot (1:2.1.7-8) experimental; urgency=low
+
+  * This version is not actually intended for upload.  It merely undoes
+    some changes made for the the wheezy release.  Namely, the following
+    features are back:
+    - TCP Wrappers support
+    - Hurd compatibility support
+    - Triggers.
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Thu, 13 Jun 2013 16:14:21 -0400
+
+dovecot (1:2.1.7-7) unstable; urgency=high
+
+  * If you are upgrading from stable or earlier versions of this package
+    from testing/unstable please carefully read
+    /usr/share/doc/dovecot-core/README.Debian.gz for important information
+    about changes.
+  * [0d74b31] Move Breaks/Replaces mailavenger from dovecot-common to
+    dovecot-core (Closes: #694376)
+  * [a8030a1] Revamped dovecot-cores README.Debian by adding any info I could
+    think of in order to ease upgrade problems. (Closes: #696820)
+  * [04798d3] Don't touch 10-ssl.conf at all.  Eventually I will DTRT with
+    regards to the default generated ssl certificates but in the mean time
+    this will do the least mischief.  (Closes: #696817)
+  * [fde17d1] Patch to make /etc/dovecot/readme point to the right place for
+    the example configuration.  (Closes: #698941)
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Mon, 04 Feb 2013 16:27:17 -0500
+
+dovecot (1:2.1.7-6) unstable; urgency=high
+
+  * WARNING: in order to get this package into wheezy some functionality
+    from the previous release had to be removed.  Namely:
+    - TCP Wrappers support
+    - Hurd compatibility support
+    - Triggers
+    All this will be coming back in the next version but for now, if you need
+    any of it, stick with -5.
+  * [1f869e0] NEWS.Debian was not getting added to the package (Closes: #693621)
+  * [564c5e2] Add breaks and replaces mailavenger for debian-common
+    (Closes: #694376)
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Fri, 14 Dec 2012 17:01:33 -0500
+
+dovecot (1:2.1.7-5) unstable; urgency=high
+
+  * [132bc3b] Remove call to ntp-wait in init script.  Dovecot handles a
+    skewed clock much better now.  (Closes: #693225)
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Wed, 14 Nov 2012 14:45:29 -0500
+
+dovecot (1:2.1.7-4) unstable; urgency=high
+
+  * [68ef7ad] piuparts complained /etc/dovecot/private was left unowned on
+    purge which is against policy (Closes: #692944)
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Sun, 11 Nov 2012 23:45:07 -0500
+
+dovecot (1:2.1.7-3) unstable; urgency=high
+
+  [ Jaldhar H. Vyas ]
+  * [03d5499] Do not generate new dovecot cert if key or cert is already
+    present (Closes: #685896,#631257) Thanks to Alexander Ufimtsev
+    <alexu@ucd.ie> and Jörg-Volker Peetz <jvpeetz@web.de> for patches.
+  * [5be6c2a] You should be able to upgrade and remove dovecot-managesieved
+    without errors (Closes: #665487)
+  * [1a9ad5c] Fixes instances where UTF-8 was misused instead of mUTF-7
+    (Closes: #680035)
+  * [c0ac3ba] Backport of fix for failure to autocreate mailboxes
+    (Closes: #623440) This also requires setting the default mail_location.
+  * [ba1d3f5] Fix FTBFS on Hurd (Closes: #686931) via upstream patch backported
+    from mercurial.
+  * [cfa92b5] Upgrade pigeonhole page to 0.3.1 (Closes: #688407)
+  * [58f01c2] Generated certificates will now be created in /etc/dovecot
+    (Closes: #608719)
+  * [8e40ea5] Patch to build with PIE and bindnow
+    by intrigeri@debian.org (Closes: #679017)
+  * [87d982b] Use libwrap (Closes: #685850)
+  * [ebb5421] Start after nslcd (Closes: #692632)
+  * [2db5c1a] Add support for dpkg triggers.  This means dovecot will not be
+    repeatedly restarted when installing or removing lots of plugins
+    (Closes: #601744)
+  * [bc66629] Patch to document -k in dsync.1
+    by Luca Capello <luca@pca.it> (Closes: #680992)
+  * [ba67766] Add lintian overrides.
+
+  [ Marco Nenciarini ]
+  * [ffba408] Updated watch file
+  * [9777434] Install missing default configuration files from upstream
+  * [9496828] Backport fix for segfault in managesieve triggered by
+    CHECKSCRIPT command. (Closes: #688197)
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Sat, 10 Nov 2012 03:50:30 -0500
+
+dovecot (1:2.1.7-2) unstable; urgency=low
+
+  * [e23a136] Just a quick upload to make dovecot buildable on non-linux
+    platforms again. (Closes: #676817) I did it by removing the systemd
+    build-dep altogether.  sd-daemon.[ch] is included in the source so if
+    systemd is installed on a system, it should be detected and socket
+    activation should happen.  That is, assuming I've got everything right
+    (and documented) which is something I'll be looking into for the next
+    version.
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Thu, 21 Jun 2012 23:54:47 -0400
+
+dovecot (1:2.1.7-1) unstable; urgency=low
+
+  * [7668742] Imported upstream 2.1.7 (Closes: #663243)
+              + doveadm.1 documents the move command. (Closes: #641750)
+  * [4db927a] Patch to enable systemd support.  (Closes: #672266)
+              Thanks Riku Voipio.
+  * [e17ac86] Previous attempt at setting hardening flags did not include the
+              drac plugin.  Simon Ruderich provided an extra patch.
+              (Closes: #653530)
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Wed, 30 May 2012 16:27:21 -0400
+
+dovecot (1:2.1.4-1) experimental; urgency=low
+
+  * [6cc1e7d] Imported upstream 2.1.4
+
+ -- Micah Anderson <micah@debian.org>  Tue, 10 Apr 2012 11:33:05 -0400
+
+dovecot (1:2.1.3-1) experimental; urgency=low
+
+  * [49ecc33] Imported upstream 2.1.3
+
+ -- Micah Anderson <micah@debian.org>  Fri, 16 Mar 2012 22:04:46 -0400
+
+dovecot (1:2.1.2-1) experimental; urgency=low
+
+  * [70c3f6e] Imported upstream 2.1.2
+  * Add missing dependency on dpkg-dev 1.16.1
+
+ -- Micah Anderson <micah@debian.org>  Thu, 15 Mar 2012 20:26:35 -0400
+
+dovecot (1:2.1.1-1) experimental; urgency=low
+
+  * [e113636] Imported upstream 2.1.1
+  * Updated pigeonhole patch to 0.3.0, supporting 2.1
+
+ -- Micah Anderson <micah@debian.org>  Tue, 13 Mar 2012 23:08:06 -0400
+
+dovecot (1:2.0.18-1) unstable; urgency=low
+
+  * [85ae320] Imported Upstream version 2.0.18
+  * [9cfd1da] Upped standards version to 3.9.3
+  * [afb4164] Patch to dovecot-core/postinst so that permissions of symlinked
+    certificates aren't modified. (Closes: #646508)
+    Thanks Michael Kuhn.
+  * [bf642ee] Patch to enable hardened build flags.  (Closes: #653530)
+    Thanks Moritz Muehlenhoff.
+  * [00b0d0c] Updated pigeonhole to 0.2.6
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Fri, 09 Mar 2012 00:55:13 -0500
+
+dovecot (1:2.0.15-1) unstable; urgency=low
+
+  * [a22575a] New upstream version 2.0.15: (Closes: #642045)
+      + doveadm altmove: Added -r parameter to move mails back to primary
+        storage.
+      - v2.0.14: Index reading could have eaten a lot of memory in some
+        situations
+      - doveadm index no longer affects future caching decisions
+      - mbox: Fixed crash during mail delivery when mailbox didn't yet have
+        GUID assigned to it.
+      - zlib+mbox: Fetching last message from compressed mailboxes crashed.
+      - lib-sql: Fixed load balancing and error
+  * [8ce5abc] Update pigeonhole to release 0.2.4
+  * [87658d2] Add dovecot-solr to dovecot-core's Suggests line.
+
+ -- Marco Nenciarini <mnencia@debian.org>  Mon, 19 Sep 2011 19:26:48 +0200
+
+dovecot (1:2.0.14-3) unstable; urgency=low
+
+  * [f37a9ec] Enable solr full text search plugin. (LP: #620959)
+  * [cdd8b84] Build dovecot-common as architecture-all package.
+  * [af90444] Fix mail_plugin_dir default value in conf.d/10-mail.conf
+    (Closes: #624294)
+  * [61347cb] Bump debhelper build-depends to (>= 7.2.3~) because we use
+    dh_bugfiles.
+  * [4b757b6] Fix wrong configuration files path in sieve manpages.
+
+ -- Marco Nenciarini <mnencia@debian.org>  Fri, 16 Sep 2011 02:26:21 +0200
+
+dovecot (1:2.0.14-2) unstable; urgency=low
+
+  [ Marco Nenciarini ]
+  * [a6896d6] Rename dovecot-common to dovecot-core. (Closes: #624835)
+  * [c36ca96] Manage protocols during package configuration and
+    deconfiguration phases.
+  * [599b55b] Update pigeonhole to release 0.2.3.
+  * [c654db0] Add a bug-script which will append doveconf -n output to bug
+    reports.
+  * [5270323] Add notes about upgrading from 1.2 to README.Debian.
+    (Closes: #635351)
+  * [5cd77a3] Add build-arch and build-indep target to debian/rules as
+    required by lintian.
+  * [be05d2a] Avoid ucf question when upgrading from squeeze and the only
+    difference in dovecot.conf is at the "protocols" line.
+  * [3c73782] Remove ucf backups during purge.
+  * [3ccf508] Make /etc/dovecot/*.ext readable by dovecot group members.
+    (Closes: #639005)
+  * [665bcc5] Use -c ${CONF} when calling doveconf to determine PIDBASE.
+    Thanks to Jonathan Hall (Closes: #627794)
+  * [325042c] Purge obsolete unmodified config files from /etc.
+    (Closes: #629397)
+  * [a9b9afb] Modified init script to report failures. (Closes: #629654)
+  * [d4622fd] Make dovecot-core suggesting all other packages containing
+    additional features.
+
+ -- Marco Nenciarini <mnencia@debian.org>  Thu, 15 Sep 2011 19:43:55 +0200
+
+dovecot (1:2.0.14-1) unstable; urgency=low
+
+  * [50a947e] New upstream version 2.0.14: (Closes: #640143)
+      + doveadm: Added support for running mail commands by proxying to
+        another doveadm server.
+      + Added "doveadm proxy list" and "doveadm proxy kick" commands to
+        list/kick proxy connections (via a new "ipc" service).
+      + Added "doveadm director move" to assign user from one server to
+        another, killing any existing connections.
+      + Added "doveadm director ring status" command.
+      + userdb extra fields can now return name+=value to append to an
+        existing name, e.g. "mail_plugins+= quota".
+      - script-login attempted an unnecessary config lookup, which usually
+        failed with "Permission denied".
+      - lmtp: Fixed parsing quoted strings with spaces as local-part for
+        MAIL FROM and RCPT TO.
+      - imap: FETCH BODY[HEADER.FIELDS (..)] may have crashed or not
+        returned all data sometimes.
+      - ldap: Fixed random assert-crashing with with sasl_bind=yes.
+      - Fixes to handling mail chroots
+      - Fixed renaming mailboxes under different parent with FS layout when
+        using separate ALT, INDEX or CONTROL paths.
+      - zlib: Fixed reading concatenated .gz files.
+  * [0297425] Use xz compression for dbg packages.
+  * [4464ccf] Support parallel builds in debian/rules
+  * [19667f0] Acknowledge NMU 1:2.0.13-1.1.
+    Thanks to Luk Claes
+
+ -- Marco Nenciarini <mnencia@debian.org>  Tue, 06 Sep 2011 23:18:46 +0200
+
+dovecot (1:2.0.13-1.1) unstable; urgency=low
+
+  * Non-maintainer upload.
+  * Don't ship .la files (Closes: #621297).
+
+ -- Luk Claes <luk@debian.org>  Sat, 18 Jun 2011 12:31:28 +0200
+
+dovecot (1:2.0.13-1) unstable; urgency=high
+
+  [ Marco Nenciarini ]
+  * [8af9e4d] New upstream version 2.0.13:
+      + Added "doveadm index" command to add unindexed messages into
+        index/cache. If full text search is enabled, it also adds unindexed
+        messages to the fts database.
+      + added "doveadm director dump" command.
+      + pop3: Added support for showing messages in "POP3 order", which can
+        be different from IMAP message order. This can be useful for
+        migrations from other servers. Implemented it for Maildir as 'O'
+        field in dovecot-uidlist.
+      - doveconf: Fixed a wrong "subsection has ssl=yes" warning.
+      - mdbox purge: Fixed wrong warning about corrupted extrefs.
+      - sdbox: INBOX GUID changed when INBOX was autocreated, leading to
+        trouble with dsync.
+      - script-login binary wasn't actually dropping privileges to the
+        user/group/chroot specified by its service settings.
+      - Fixed potential crashes and other problems when parsing header names
+        that contained NUL characters. (CVE-2011-1929)
+    (Closes: #627443)
+
+ -- Marco Nenciarini <mnencia@debian.org>  Sat, 21 May 2011 23:58:06 +0200
+
+dovecot (1:2.0.12-1) unstable; urgency=low
+
+  [ Jaldhar H. Vyas ]
+  * [66cbb94] New upstream major release.  Imported Upstream version 2.0.12
+  * [a48c7fd] Updated standards version
+
+  [ Marco Nenciarini ]
+  * [5c6b334] Updated watch file
+  * [0ba683e] Updated init script to new configuration scheme. (Closes: #593527)
+  * [8370be4] Fix upgrading from rename-it.nl packages
+  * [ae804e3] Update changelog
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Mon, 25 Apr 2011 17:03:26 -0400
+
+dovecot (1:2.0.11-1) experimental; urgency=low
+
+  * [a4764a4] New upstream major release (Closes: #573889, #580929,
+    #597476, #603951, #606089, #606646, #606649, #608075, #612369, #610919)
+  * [461f667] Updated copyright and package descriptions. Thanks Stephan
+    Bosch.
+  * [7fbd92a] Drop all patches but drac support. Update pigeonhole to
+    release 0.2.1
+  * [05a5a1e] Update debian/rules to work with dovecot 2.0. Thanks
+    Stephan Bosch.
+  * [a36193e] Update debian/dovecot-common.{postinst,postrm} to handle
+    the new config file layout. Thanks Stephan Bosch.
+  * [3589afd] Remove debian/dovecot.1 manpage because is provided by
+    upstream.
+  * [f1703e1] Remove debian/dovecotpw.1 manpage as there is no dovecotpw
+    executable in dovecot 2.0. Same feature is provided through 'doveadm
+    pw' command.
+  * [e30283d] Remove expire-tool.sh wrapper as there is no expire-tool
+    in dovecot 2.0. Same feature is provided through 'doveadm expunge'
+    command.
+  * [3385061] Remove daily cron script and the related configuration
+    from the default file.
+  * [bec19a0] Update debian/dovecot-common.NEWS.Debian with upgrading
+    instructions
+  * [1e56f1c] Call dh_makeshlibs with --no-script option to avoid
+    {postinst,postrm}-has-useless-call-to-ldconfig lintian errors.
+  * [325f055] Disable numbers in patches (requires git-buildpackage >=0.5.13)
+  * [74d6ebe] Added debian/source/local-options with "unapply-patches".
+  * [0edef17] Set default protocols value as empty and enable each
+    protocol in its own configuration file in
+    /usr/share/dovecot/protocols.d (LP: #661453)
+  * [e88860c] Removed code about upgrading from pre-etch packages.
+  * [95c5d47] Removed obsolete Build-Conflict (Closes: #614053)
+  * [41dddf4] Move configurations for imapd and pop3d to the appropriate
+    packages. (Closes: #606648)
+  * [b8b66dc] Removed obsolete README.Debian and NEWS.Debian from
+    dovecot-pop3d and dovecot-imapd packages
+  * [0bda8ac] Added packages dovecot-lmtpd and dovecot-managesieved.
+  * [9a954f1] Build databases, ldap and gssapi support as plugins
+  * [1a3f323] Deregister obsolete configuration files from ucf in
+    dovecot-common postinst on upgrades.
+  * [fae4b76] Added packages dovecot-pgsql, dovecot-mysql, dovecot-sqlite,
+    dovecot-ldap, dovecot-gssapi and dovecot-sieve (Closes: #251433)
+  * [aa8983d] Imported Upstream version 2.0.11
+
+ -- Marco Nenciarini <mnencia@debian.org>  Mon, 07 Mar 2011 18:23:59 +0100
+
+dovecot (1:1.2.15-3) unstable; urgency=low
+
+  * [096c373] Fix file overwrite error when upgrading from lenny
+    (Closes: #601980)
+  * [37b3220] Add --no-create-home to adduser invocation in dovecot-
+    common.postinst script (Closes: #601767)
+  * [b00b527] Fall back to `hostname` if `hostname -f` invocation fails
+    (Closes: #562780)
+  * [9102fec] Make self-generated certificates key length 2048 bits.
+
+ -- Marco Nenciarini <mnencia@debian.org>  Tue, 02 Nov 2010 11:12:53 +0100
+
+dovecot (1:1.2.15-2) unstable; urgency=low
+
+  * [a0744a5] Switch vcs fields to git
+  * [d8a1bd4] Add debian/gbp.conf to make easy the usage of git-
+    buildpackage
+  * [c0dc21f] Warn that the generated SSL certificate will expire in 10
+    years instead of 365 days, as it's the lifetime of of self-generated
+    certificates since 1:1.2.13-1
+  * [6472d63] Switch to gbp-pq patch management.
+  * [bf2fc17] Upgraded sieve plugin to version 0.1.18
+  * [6b9809d] Upgraded managesieve plugin to version 0.11.12
+  * [d67cd17] Upgraded dovecot-managesieve.patch to version 0.11.12
+
+ -- Marco Nenciarini <mnencia@debian.org>  Thu, 28 Oct 2010 22:50:19 +0200
+
+dovecot (1:1.2.15-1) unstable; urgency=high
+
+  [ Marco Nenciarini ]
+  * New upstream release (Closes: #587036,#597529)
+  * Updated policy version to 3.9.1.0 (no changes needed)
+
+  [ Jaldhar H. Vyas ]
+  * [SECURITY] Fixes two bugs with acls which could have allowed a user to
+    gain improper access or admin rights to shared mailboxes.
+    (Closes: #599521)
+  * Warn that the generated SSL certificate will expire in 365 days.  Thanks
+    Phillip Weis.  (Closes: #576455)
+  * Wherever the path to sendmail is given as /usr/lib/sendmail, change to
+    /usr/sbin/sendmail.  (Closes: #570814,#595671)
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Fri, 08 Oct 2010 17:34:19 -0400
+
+dovecot (1:1.2.13-1) unstable; urgency=low
+
+  [ Jaldhar H. Vyas ]
+  * dovecot-common: Upped expiration date of self-generated certificates to
+    10 years from 1 year.  (Closes: #587371)
+
+  [ Marco Nenciarini ]
+  * New upstream release:
+    - Fixed iconv() crash when it was processing several kilobytes of
+      broken continuous input. This mainly could have caused a problem
+      with IMAP SEARCH. Possibly also with some Sieve checks.
+    - If MIME encoded-words contained line feeds, Dovecot logged
+      cache corruption errors.
+    - mbox: Renaming mailbox under newly created dir didn't move index
+      directory.
+    - mbox: When generating envelope to From_-line, don't append a second
+      @owndomain if username already has one.
+  * Updated policy version to 3.9.0.0 (no changes needed)
+
+ -- Marco Nenciarini <mnencia@debian.org>  Mon, 26 Jul 2010 17:03:38 +0200
+
+dovecot (1:1.2.12-1) unstable; urgency=low
+
+  * New upstream release
+  * Upgraded sieve plugin to version 0.1.17
+  * Refreshed dovecot-managesieve.patch
+  * debian/patches/sieve-fix-imapflags.patch: Removed. Fixed upstream.
+
+ -- Marco Nenciarini <mnencia@debian.org>  Sat, 26 Jun 2010 18:14:15 +0200
+
+dovecot (1:1.2.11-1) unstable; urgency=high
+
+  * New upstream release
+  * urgency set to high, because under particular circumstances you could
+    cause a DoS by sending a message with a huge header.
+  * sieve: Fix addflags command in deprecated imapflags extension
+    (Closes: #570058)
+  * Include a daily expire script with a setting in /etc/default/dovecot
+    to enable it. The new script is disabled by default as it requires
+    some configurations by the system administrator. Thanks to Hans Spaans
+  * Mark expire-tool.sh as a bash script
+  * Bump Standards-Version to 3.8.4, no changes needed
+
+ -- Marco Nenciarini <mnencia@debian.org>  Mon, 08 Mar 2010 22:25:46 +0100
+
+dovecot (1:1.2.10-1) unstable; urgency=low
+
+  * New upstream release (Closes: #566796)
+  * Upgraded sieve plugin to version 0.1.15
+  * Upgraded managesieve plugin to version 0.11.11
+  * Upgraded dovecot-managesieve.patch to version 0.11.11
+
+ -- Marco Nenciarini <mnencia@debian.org>  Mon, 25 Jan 2010 17:21:09 +0100
+
+dovecot (1:1.2.9-2) unstable; urgency=low
+
+  * Upgraded sieve plugin to version 0.1.14
+  * Upgraded managesieve plugin to version 0.11.10
+  * Upgraded dovecot-managesieve.patch to version 0.11.10
+  * debian/patches/dovecot-example.patch: Refreshed.
+  * Added expire-tool wrapper script (Closes: #565538)
+  * Added ${misc:Depends} to the dependencies of each binary package,
+    to make lintian happy (debhelper-but-no-misc-depends)
+
+ -- Marco Nenciarini <mnencia@debian.org>  Sun, 17 Jan 2010 09:39:42 +0100
+
+dovecot (1:1.2.9-1) unstable; urgency=low
+
+  * New upstream release.
+  * debian/patches/gold-fix.patch: Removed. Fixed upstream.
+
+ -- Marco Nenciarini <mnencia@debian.org>  Thu, 17 Dec 2009 10:52:53 +0100
+
+dovecot (1:1.2.8-2) unstable; urgency=low
+
+  * Moved libexec to lib corrections in dovecot-managesieve.patch and
+    dovecot-managesieve-dist.patch to dovecot-example.patch
+  * debian/patches/dovecot-mboxlocking.patch: Regenerated to avoid FTBFS
+    when quilt isn't installed.
+  * debian/patches/quota-mountpoint.patch: Removed. Not needed anymore.
+  * debian/patches/dovecot-quota.patch: Removed. Quotas aren't properly
+    enabled unless mail_plugins = quota imap_quota.
+  * debian/patches/gold-fix.patch: Fixed configure script to build even
+    with binutils-gold or --no-add-needed linker flag (Closes: #554306)
+  * debian/dovecot-common.init: fixed LSB headers. Thanks to Pascal Volk.
+    (Closes: #558040)
+  * debian/changelog: added CVE references to previous changelog entry.
+  * debian/rules: checked up the build system. It's not fragile anymore.
+    (Closes: 493803)
+  * debian/dovecot-common.postinst: Now invoking dpkg-reconfigure
+    on dovecot-common is enough to generate new certificates
+    if the previous ones were removed. (Closes: #545582)
+  * debian/rules: No longer install convert-tool in /usr/bin.
+    It isn't an user utility and it should stay in /usr/lib/dovecot
+    like all other similar tool.
+
+ -- Marco Nenciarini <mnencia@debian.org>  Tue, 08 Dec 2009 12:24:04 +0100
+
+dovecot (1:1.2.8-1) unstable; urgency=high
+
+  [ Marco Nenciarini ]
+  * New upstream release. (Closes: #557601)
+  * [SECURITY] Fixes local information disclosure and denial of service.
+    (see: http://www.dovecot.org/list/dovecot-news/2009-November/000143.html
+    and CVE-2009-3897)
+  * Added myself to uploaders.
+  * Switched to the new source format "3.0 (quilt)":
+    - removed dpatch from build-depends
+    - removed debian/README.source because now we use only standard
+      dpkg features
+    - regenerated all patches
+  * Prepared to switch to multi-origin source:
+    - recreated dovecot-libsieve.patch and dovecot-managesieve-dist.patch
+      starting from the upstream tarball
+    - removed all autotools related build-depends and build-conflict
+    - renamed dovecot-libsieve and dovecot-managesieve directories
+      to libsieve and managesieve.
+  * debian/rules: Moved the configuration of libsieve and managesieve from
+    the build phase to the configuration phase
+
+  [ Jaldhar H. Vyas ]
+  * Added dovecot-dbg package  with debugging symbols.  Thanks Stephan Bosch.
+    (Closes: #554710)
+  * Fixed some stray libexec'isms in the default configuration.
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Mon, 23 Nov 2009 17:04:14 -0500
+
+dovecot (1:1.2.7-1) unstable; urgency=low
+
+  * New upstream release.
+  * debian/dovecot-common.init:
+    - use $CONF when starting the daemon. (Closes: #549944)
+    - always output start/stop messages. (Closes: #523810)
+
+ -- Fabio Tranchitella <kobold@debian.org>  Sat, 14 Nov 2009 11:28:33 +0100
+
+dovecot (1:1.2.6-1) unstable; urgency=low
+
+  * New upstream release.
+  * debian/patches/v1.2.5-exec-mail_fix.dpatch: removed, merged upstream.
+
+ -- Fabio Tranchitella <kobold@debian.org>  Mon, 12 Oct 2009 06:56:55 +0000
+
+dovecot (1:1.2.5-2) unstable; urgency=low
+
+  * debian/dpatches/v1.2.5-exec-mail_fix.dpatch: added.
+    (Closes: #546694, #546695)
+  * debian/dovecot-common.dirs: removed /etc, /etc/ssl/{private,certs}, they
+    are handled by openssl; this should fix the piuparts test.
+
+ -- Fabio Tranchitella <kobold@debian.org>  Sun, 27 Sep 2009 20:53:52 +0200
+
+dovecot (1:1.2.5-1) unstable; urgency=low
+
+  * New upstream release.
+
+ -- Fabio Tranchitella <kobold@debian.org>  Mon, 14 Sep 2009 14:34:06 +0200
+
+dovecot (1:1.2.4-2) unstable; urgency=low
+
+  * debian/patches/dovecot-libsieve.dpatch: updated to 0.1.12.
+    (Closes: #539527)
+
+ -- Fabio Tranchitella <kobold@debian.org>  Wed, 02 Sep 2009 07:28:23 +0200
+
+dovecot (1:1.2.4-1) unstable; urgency=low
+
+  * New upstream release. (Closes: #537186, #540058)
+  * Bumped Standards-Version to 3.8.3, no changes needed.
+  * debian/control: only suggests ntp. (Closes: #542152)
+  * debian/patches/dovecot-managesieve-dist.dpatch: updated to the 0.11.9
+    release. (Closes: #539527)
+  * debian/rules: dovecot should be started with sequence number 20.
+    (Closes: #543473)
+
+ -- Fabio Tranchitella <kobold@debian.org>  Sat, 29 Aug 2009 17:27:59 +0200
+
+dovecot (1:1.2.2-2) unstable; urgency=high
+
+  * Er that should be fcntl not fnctl.  (Closes: #539474, #539486)
+  * For the sake of completeness, fixed some errors in the example config file.
+    (They are in the !include statements which are commented out by default
+    so it is unlikely that most users will actually be affected.)
+    (Closes: #539391)
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Sat, 01 Aug 2009 11:38:03 -0400
+
+dovecot (1:1.2.2-1) unstable; urgency=low
+
+  [Joel Johnson]
+  * New upstream version, updated ManageSieve patch
+     - fixes index corruption condition (Closes: #537388)
+  * Set default for mbox_write_locks to "fnctl dotlock" according to
+    Debian Policy (Closes: #537326)
+  * Set MySQL build dependency to use version-agnostic -dev package
+  * Drop postgres-configure patch, functionality included upstream
+  * Updated dovecot-libsieve to version 0.1.9
+     - fixes subaddress matching (Closes: #537386)
+     - check additional address-list headers (Closes: #537379)
+
+  [Jaldhar H. Vyas]
+  * Remove unneeded build-dependencies on byacc and flex now that we no longer
+    use cmusieve.
+
+ -- Joel Johnson <mrjoel@lixil.net>  Tue, 28 Jul 2009 20:35:57 -0600
+
+dovecot (1:1.2.1-2) unstable; urgency=low
+
+  * Update to use default automake (Closes: #536880) now that current upstream
+    successfully builds with 1.10 (was previously a problem in bug 473754.
+  * Updated Standards-Version to 3.8.2 (no action required)
+  * Remove ./var/run directories to fix lintian warnings.
+  * Updated dovecot-libsieve patch to version 0.1.8, removed autogen.sh,
+    tests, and doc/rfc
+  * Updated dovecot-managesieve patch to version 0.11.7
+  * Updated dovecot-managesieve-dist patch to Mercurial revision 12b9733ee8b0
+      (0.11.7+), removed lib-cmusieve, doc/rfs, autogen.sh
+  * Add additional upgrading note to README.Debian to clarify the ManageSieve
+    configuration changes needed - existing configurations break if not
+    updated! (Closes: #537158)
+  * Include the ChangeLog and README for sieve and ManageSieve
+
+ -- Joel Johnson <mrjoel@lixil.net>  Wed, 15 Jul 2009 20:29:33 -0600
+
+dovecot (1:1.2.1-1) unstable; urgency=low
+
+  [ Joel Johnson ]
+  * New upstream release
+  * Update packaging for 1.2 release
+    - update SIEVE patch to use Dovecot rewrite (version 0.1.7,
+        removed RFCs and tests for a DFSG patch)
+    - update ManageSieve patch and software to hg revision 2c9b4b4ab6a8
+        (0.11.6+) for 1.2.1 support
+        + removed lib-cmusieve since building against newer sieve
+        + removed doc/rfc for DFSG compliance
+        + removed autogen.sh since we run autotools during the build
+    - updated other misc patches
+
+ -- Joel Johnson <mrjoel@lixil.net>  Sun, 12 Jul 2009 11:15:01 -0600
+
+dovecot (1:1.1.16-1) unstable; urgency=low
+
+  * New upstream release. (Closes: #531599)
+
+ -- Fabio Tranchitella <kobold@debian.org>  Thu, 04 Jun 2009 12:38:59 +0200
+
+dovecot (1:1.1.15-1) unstable; urgency=low
+
+  * New upstream release; sorry, no time to include extra upstream patches
+    which are not released, feel free to submit them as dpatch files and I will
+    add them to the package. (Closes: #529923)
+  * debian/control: updated Standards-Version, no changes needed.
+  * debian/dovecot-common.init: patched to support ENABLED=0 in
+    /etc/default/dovecot, useful if dovecot is used as local-only IMAP server
+    through PREAUTH interface. (Closes: #524302)
+  * debian/dovecot-common.README.Debian: simplified, we only point the user to
+    wiki.dovecot.org; added a note about how to regenerate the SSL certificate.
+    (Closes: #528934)
+  * debian/dovecot-common.postrm: do not break if /etc/ssl/certs does not
+    exist. (Closes: #524865)
+  * debian/patches/dovecot-managesieve-dist: updated to 0.10.6.
+
+ -- Fabio Tranchitella <kobold@debian.org>  Sun, 31 May 2009 20:13:26 +0200
+
+dovecot (1:1.1.13-2) unstable; urgency=high
+
+  * New upload, urgency set to high: the package in testing is broken.
+  * debian/dovecot-common.init: fixed a typo.
+  * debian/patches/dovecot-example.dpatch: fixed a few paths.
+    (Closes: #521544)
+
+ -- Fabio Tranchitella <kobold@debian.org>  Sun, 29 Mar 2009 21:16:02 +0200
+
+dovecot (1:1.1.13-1) unstable; urgency=low
+
+  * New upstream release.
+  * This version fixes problems accessing mailboxes in some setups.
+    (Closes: #520310)
+
+ -- Fabio Tranchitella <kobold@debian.org>  Thu, 19 Mar 2009 17:31:56 +0100
+
+dovecot (1:1.1.12-1) unstable; urgency=low
+
+  * New upstream release, it fixes some wrong defaults in the configuration
+    file. (Closes: #518631)
+  * debian/rules: add support for libdb. (Closes: #518630)
+  * debian/dovecot-common.postinst: create the dovecot certificate only at
+    install time. (Closes: #518738, #518598)
+
+ -- Fabio Tranchitella <kobold@debian.org>  Mon, 16 Mar 2009 14:26:32 +0100
+
+dovecot (1:1.1.11-4) unstable; urgency=low
+
+  * debian/dovecot-common.init: fixed a typo from the last upload.
+    (Closes: #518504)
+
+ -- Fabio Tranchitella <kobold@debian.org>  Fri, 06 Mar 2009 18:39:09 +0100
+
+dovecot (1:1.1.11-3) unstable; urgency=low
+
+  * debian/dovecot-common.init: applied patch from HÃ¥kon Stordahl to call
+    ntp-wait if available. (Closes: #517808)
+
+ -- Fabio Tranchitella <kobold@debian.org>  Wed, 04 Mar 2009 17:38:59 +0100
+
+dovecot (1:1.1.11-2) unstable; urgency=low
+
+  * debian/dovecot-common.init: fixed a bug in the init script, adding a
+    missing slash in the PIDFILE path. (Closes: #516845)
+  * debian/dovecot-common.init: fixed a bug in the init script, sending the HUP
+    signal for reloading the configuration. (Closes: #512197)
+  * debian/dovecot-common.init: check if /etc/inetd.conf exists.
+    (Closes: #509259, #497148)
+  * debian/control: dovecot-common suggests ntp and ntpdate. (Closes: #511060)
+  * debian/dovecot-common.postinst: migration hook for default_mail_env =>
+    mail_location. (Closes: #517073)
+  * debian/dovecot.1: fixed the path of the configuration file.
+    (Closes: #501493)
+
+ -- Fabio Tranchitella <kobold@debian.org>  Fri, 27 Feb 2009 17:24:09 +0100
+
+dovecot (1:1.1.11-1) unstable; urgency=low
+
+  * New upstream release, upload to unstable. (Closes: #507122)
+  * debian/patches/dovecot-quota.dpatch: fixed a bug in the imap quota plugin.
+    (Closes: #484677)
+  * debian/dovecot-common.init: added the status action, thanks Fladischer
+    Michael for the patch. (Closes: #509694)
+  * debian/dovecotpw.1: added manpage for dovecotpw, thanks Xavier Luthi for
+    the patch. (Closes: #504712)
+
+ -- Fabio Tranchitella <kobold@debian.org>  Fri, 20 Feb 2009 20:39:38 +0100
+
+dovecot (1:1.1.9-1) experimental; urgency=low
+
+  [ Fabio Tranchitella ]
+  * debian/control: dovecot-common suggests ntp.
+
+  [ Joel Johnson ]
+  * New upstream release
+  * updated managesieve patch to apply against new version
+
+ -- Joel Johnson <mrjoel@lixil.net>  Sat, 24 Jan 2009 04:12:42 +0000
+
+dovecot (1:1.1.8-1) experimental; urgency=low
+
+  * New upstream release.
+  * debian/control: added LDA to the description of dovecot-common.
+
+ -- Fabio Tranchitella <kobold@debian.org>  Wed, 07 Jan 2009 23:14:29 +0100
+
+dovecot (1:1.1.7-1) experimental; urgency=low
+
+  * New upstream release
+  * Updated dovecot-ssh.patch for new release
+  * Updated MANAGESIEVE to 0.10.4
+  * Fix package to support double compilation
+    - Properly clean dovecot-managesieve as pointed out by Stephan Bosch
+    - Add --copy directive to automake invocation
+
+ -- Joel Johnson <mrjoel@lixil.net>  Thu, 04 Dec 2008 20:52:11 -0700
+
+dovecot (1:1.1.2-3) experimental; urgency=low
+
+  * debian/control: added libbz2-dev to the Build-Depends to enable the bzip2
+    support. (Closes: #495129)
+
+ -- Fabio Tranchitella <kobold@debian.org>  Thu, 14 Aug 2008 20:45:41 +0200
+
+dovecot (1:1.1.2-2) experimental; urgency=low
+
+  * Merged changes in the unstable package.
+  * debian/control: added replaces for the imap and pop modules from the
+    -common to the the -imap and -pop packages. (Closes: #493798)
+  * debian/rules: applied some of the suggestions from Matthias Kloses,
+    thanks! (Refs: #493803)
+
+ -- Fabio Tranchitella <kobold@debian.org>  Tue, 05 Aug 2008 21:20:33 +0200
+
+dovecot (1:1.1.2-1) experimental; urgency=low
+
+  * New upstream release
+  * Trivial update to autocreate patch
+
+ -- Joel Johnson <mrjoel@lixil.net>  Sat, 26 Jul 2008 08:18:39 -0600
+
+dovecot (1:1.1.1-1) experimental; urgency=low
+
+  [ Joel Johnson ]
+  * New upstream release (Closes: #487989)
+  * Updated policy version to 3.8.0.1 (no changes needed)
+  * Updated watch file to watch new release directory
+  * Actually install dovecot.8 since we have it
+  * Removed quota_v2 patch (was unreachable code): upstream now defaults
+    to quota v1 if _LINUX_QUOTA_VERSION is not defined
+  * Removed pam-error-information patch: included upstream
+  * Removed mbox_snarf patch: included upstream
+  * Removed full MANAGESIEVE patch (replaced with new 1.1 approach)
+  * Merged protocols_none_by_default patch into dovecot-example patch
+  * Updated drac patch to build against 1.1
+  * Updated autocreate patch with 1.1 version
+  * Updated dovecot-sieve to 1.1.5
+  * Added dovecot-managesieve patch against dovecot tree
+  * Added dovecot-managesieve source for building module
+
+  [ Fabio Tranchitella ]
+  * Added Joel Johnson to the uploaders; thanks for your work!
+
+ -- Fabio Tranchitella <kobold@debian.org>  Sun, 20 Jul 2008 19:09:23 +0200
+
+dovecot (1:1.0.15-2) unstable; urgency=low
+
+  * debian/dovecot.1: added "This includes doing a syntax check" to the -a
+    option.
+  * debian/rules: now the package builds two times in a row without problems.
+    (Closes: #490201)
+
+ -- Fabio Tranchitella <kobold@debian.org>  Sun, 27 Jul 2008 19:56:18 +0200
+
+dovecot (1:1.0.15-1) unstable; urgency=low
+
+  * New upstream release.
+  * debian/rules: clean the package before unpatching. (Closes: #490201)
+
+ -- Fabio Tranchitella <kobold@debian.org>  Sun, 22 Jun 2008 09:06:01 +0200
+
+dovecot (1:1.0.14-1) unstable; urgency=low
+
+  * New upstream release.
+  * debian/patches/inbox_creation.dpatch: removed, merged upstream.
+  * debian/patches/allow_nets.dpatch: removed, merged upstream.
+
+ -- Fabio Tranchitella <kobold@debian.org>  Tue, 03 Jun 2008 10:07:13 +0200
+
+dovecot (1:1.0.13-6) unstable; urgency=low
+
+  * debian/patches/inbox_creation.dpatch: added, use mail_privileged_group's
+    group when creating inboxes if the unprivileged user fails; upstream:
+    http://hg.dovecot.org/dovecot-1.0/rev/932768a879c6 (Closes: #471716)
+
+ -- Fabio Tranchitella <kobold@debian.org>  Wed, 28 May 2008 12:58:17 +0200
+
+dovecot (1:1.0.13-5) unstable; urgency=low
+
+  * debian/patches/allow_nets.dpatch: added, allow_nets didn't work correctly
+    with big endian machines; patch from upstream, thanks Timo:
+    http://hg.dovecot.org/dovecot-1.0/rev/71c02fdf1b59
+
+ -- Fabio Tranchitella <kobold@debian.org>  Thu, 15 May 2008 14:48:14 +0200
+
+dovecot (1:1.0.13-4) unstable; urgency=low
+
+  * debian/patches/dovecot-MANAGESIEVE-9.3.dpatch: updated managesieve to
+    version 9.3.
+  * debian/dovecot-common.README.Debian: added a note about how to configure
+    dovecot to log to file instead of using syslog.
+  * debian/dovecot.1: added a SIGNALS section. (Closes: #479059)
+  * dovecot-sieve: updated to the last hg release (5c3ba11994cb).
+    (Closes: #479104)
+
+ -- Fabio Tranchitella <kobold@debian.org>  Mon, 05 May 2008 17:28:21 +0200
+
+dovecot (1:1.0.13-3) unstable; urgency=low
+
+  * debian/rules: do not install anymore the ldap and sql example
+    configuration files under /etc. (Closes: #472674)
+  * debian/dovecot-common.postinst: really chmod
+    /etc/dovecot/dovecot-{ldap,sql}.conf files to 0600.
+  * debian/devecot-common.init: do not start the service if dovecot.conf
+    doesn't exist. (Closes: #475888)
+
+ -- Fabio Tranchitella <kobold@debian.org>  Sun, 27 Apr 2008 22:42:37 +0200
+
+dovecot (1:1.0.13-2) unstable; urgency=low
+
+  * debian/rules: use aclocal-1.9 instead of aclocal. (Closes: #473754)
+
+ -- Fabio Tranchitella <kobold@debian.org>  Tue, 01 Apr 2008 15:30:32 +0200
+
+dovecot (1:1.0.13-1) unstable; urgency=high
+
+  * New upstream release, fixes a security issue:
+    http://www.dovecot.org/list/dovecot-news/2008-March/000064.html
+
+ -- Fabio Tranchitella <kobold@debian.org>  Sun, 09 Mar 2008 19:22:20 +0100
+
+dovecot (1:1.0.12-1) unstable; urgency=high
+
+  * New upstream release. (Closes: #469457)
+  * debian/patches/dovecot-MANAGESIEVE-9.2.dpatch: updated, thanks to Marco
+    Nenciarini for the patch.
+
+ -- Fabio Tranchitella <kobold@debian.org>  Thu, 06 Mar 2008 15:53:07 +0100
+
+dovecot (1:1.0.10-4) unstable; urgency=low
+
+  * debian/patches/autocreate.dpatch: added, thanks to Walter Reiner.
+  * debian/rules: use --with-ioloop=best instead of --with-ioloop=epoll, as
+    suggested by Timo. (Closes: #466296)
+
+ -- Fabio Tranchitella <kobold@debian.org>  Mon, 18 Feb 2008 09:29:39 +0100
+
+dovecot (1:1.0.10-3) unstable; urgency=low
+
+  * debian/patches/dovecot-MANAGESIEVE-9.1.dpatch: added, thanks to Aleksey
+    Midenkov for providing a patch. (Closes: #416166)
+  * debian/dovecot-common.init: added $time to Should-Start. (Closes: #461543)
+  * debian/dovecot-common.postinst: do not add the dovecot user to the mail
+    group, it is not required by upstream. (Closes: #457123)
+  * debian/control: updated Standards-Version to 3.7.3, no changes required.
+
+ -- Fabio Tranchitella <kobold@debian.org>  Sun, 10 Feb 2008 18:37:55 +0100
+
+dovecot (1:1.0.10-2) unstable; urgency=low
+
+  * debian/patches/mbox_snarf.dpatch: added, thanks to Bernd Kuhls.
+    (Closes: #462319)
+
+ -- Fabio Tranchitella <kobold@debian.org>  Thu, 24 Jan 2008 10:12:02 +0100
+
+dovecot (1:1.0.10-1) unstable; urgency=high
+
+  * New upstream release, fixes a security bug.
+  * debian/patches/exec_check_for_none.dpatch: updated.
+
+ -- Fabio Tranchitella <kobold@debian.org>  Sun, 30 Dec 2007 10:29:26 +0100
+
+dovecot (1:1.0.9-1) unstable; urgency=low
+
+  * New upstream release.
+  * debian/control: added the Vcs-Svn and Vcs-Browser fields.
+
+ -- Fabio Tranchitella <kobold@debian.org>  Wed, 12 Dec 2007 08:10:11 +0100
+
+dovecot (1:1.0.8-2) unstable; urgency=low
+
+  * Provides a dovecot-dev package, thanks to Josh Triplett for providing a
+    patch. (Closes: #444812)
+
+ -- Fabio Tranchitella <kobold@debian.org>  Tue, 04 Dec 2007 09:22:59 +0100
+
+dovecot (1:1.0.8-1) unstable; urgency=low
+
+  * New upstream release.
+  * debian/patches/unsupported-sasl-mech.dpatch: merged with upstream.
+
+ -- Fabio Tranchitella <kobold@debian.org>  Thu, 29 Nov 2007 13:36:39 +0100
+
+dovecot (1:1.0.7-3) unstable; urgency=low
+
+  * debian/patches/dovecot-ssl.dpatch: provide mechanism to discover if ssl
+    client certificate is verified, patch from Stephen Gran. (Closes: #446555)
+  * debian/patches/pam-error-information.dpatch: fill auth information in pam
+    error, patch backported from upstream RCS. (Closes: #439246)
+  * debian/patches/unsupported-sasl-mech.dpatch: should use NO (not BAD) for
+    unsupported SASL mech, patch backported from upstream RCS. (Closes: #449324)
+
+ -- Fabio Tranchitella <kobold@debian.org>  Wed, 14 Nov 2007 21:33:55 +0100
+
+dovecot (1:1.0.7-2) unstable; urgency=low
+
+  * debian/dovecot-common.postinst:
+    + don't fail if dovecot-ldap.conf or dovecot-sql.conf are removed; thanks
+      to Mathias Gug. (Closes: #448401)
+    + fix permissions of /var/run/dovecot and /var/run/dovecot/login.
+      (Closes: #446051)
+
+ -- Fabio Tranchitella <kobold@debian.org>  Sun, 04 Nov 2007 10:06:06 +0100
+
+dovecot (1:1.0.7-1) unstable; urgency=low
+
+  * New upstream release.
+  * debian/rules: remove drac.so in the clean target. (Closes: #442548)
+  * debian/dovecot-common.init: implemented the reload action.
+    (Closes: #441032)
+  * Update protocols option in configuration when installing/removing
+    -imapd/-pop3d packages. Thanks to Soren Hansen and Mathias Gug from
+    Ubuntu for providing a patch. (Closes: #447201)
+
+ -- Fabio Tranchitella <kobold@debian.org>  Fri, 02 Nov 2007 23:06:17 +0100
+
+dovecot (1:1.0.5-1) unstable; urgency=low
+
+  * New upstream release.
+
+ -- Fabio Tranchitella <kobold@debian.org>  Mon, 10 Sep 2007 09:25:59 +0200
+
+dovecot (1:1.0.3-3) unstable; urgency=low
+
+  * debian/dovecot-common.init: don't use the init script name to locate the
+    configuration file because it is not reliable. If you really want to start
+    multiple servers, just copy the init script and use the optional default
+    file to override the variables. (Closes: #437228)
+
+ -- Fabio Tranchitella <kobold@debian.org>  Thu, 16 Aug 2007 09:17:01 +0200
+
+dovecot (1:1.0.3-2) unstable; urgency=low
+
+  * debian/rules: removed the --with-notify=inotify switch, it should be
+    detected automatically.
+
+ -- Fabio Tranchitella <kobold@debian.org>  Thu, 09 Aug 2007 09:39:50 +0200
+
+dovecot (1:1.0.3-1) unstable; urgency=low
+
+  * New upstream release. (Closes: #434038, #432601)
+  * This release doesn't build dbox support out-of-the-box. (Closes: #431615)
+  * dovecot-sieve: updated to the last hg's tip. (Closes: #434079)
+  * debian/dovecot-*.README.Debian: don't call /etc/init.d scripts directly.
+    (Closes: #431991)
+  * debian/dovecot-common.init: updated with patches from Tom Metro, thanks!
+    (Closes: #426480, #422674)
+  * debian/dovecot-common.postinst: fixed a missing variable DOMAINNAME.
+    (Closes: #425917)
+  * debian/rules: moved the init script to the level 24, after the ntpdate
+    one. (Closes: #432723)
+  * debian/patches/00list: added dovecot-drac, again. (Closes: #353039)
+  * debian/rules: build with inotify and epoll support. (Closes: #419281)
+  * debian/dovecot.1: added a simple manpage for dovecot. (Closes: #426702)
+  * debian/copyright: added copyright exceptions as suggested by Timo.
+
+ -- Fabio Tranchitella <kobold@debian.org>  Sat, 04 Aug 2007 20:11:36 +0200
+
+dovecot (1:1.0.0-1) unstable; urgency=low
+
+  * New upstream release, the final 1.0.0. Bumped epoch, because we used the
+    wrong version scheming in the past and I think it is worth to do so now
+    that 1.0.0 has been released.
+  * debian/watch: updated.
+  * Rebuilt with a new glibc in unstable, now we have inotify support.
+    (Closes: #395306)
+
+ -- Fabio Tranchitella <kobold@debian.org>  Mon, 16 Apr 2007 08:42:32 +0200
+
+dovecot (1.0.rc31-1) unstable; urgency=low
+
+  * New upstream release.
+
+ -- Fabio Tranchitella <kobold@debian.org>  Mon, 09 Apr 2007 11:55:45 +0200
+
+dovecot (1.0.rc30-2) unstable; urgency=low
+
+  * debian/dovecot-common.init: check if /etc/inetd.conf exists before
+    calling sed on it. (Closes: #417299)
+
+ -- Fabio Tranchitella <kobold@debian.org>  Sun, 08 Apr 2007 16:58:30 +0200
+
+dovecot (1.0.rc30-1) unstable; urgency=low
+
+  * New upstream release.
+  *
+
+ -- Fabio Tranchitella <kobold@debian.org>  Sat, 07 Apr 2007 11:17:45 +0200
+
+dovecot (1.0.rc29-1) unstable; urgency=low
+
+  * New upstream release.
+  * debian/rules: ship convert-tool in dovecot-common. (Closes: #416909)
+
+ -- Fabio Tranchitella <kobold@debian.org>  Sat, 31 Mar 2007 14:15:39 +0200
+
+dovecot (1.0.rc28-1) unstable; urgency=low
+
+  * New upstream release.
+
+ -- Fabio Tranchitella <kobold@debian.org>  Sun, 25 Mar 2007 14:02:28 +0200
+
+dovecot (1.0.rc27-1) unstable; urgency=low
+
+  * New upstream release.
+
+ -- Fabio Tranchitella <kobold@debian.org>  Wed, 14 Mar 2007 14:39:04 +0100
+
+dovecot (1.0.rc26-4) unstable; urgency=low
+
+  * debian/dovecot-common.postinst: fixed a typo. (Closes: #414672)
+
+ -- Fabio Tranchitella <kobold@debian.org>  Tue, 13 Mar 2007 08:45:47 +0100
+
+dovecot (1.0.rc26-3) unstable; urgency=low
+
+  * debian/control: depends on ucf (>= 2.0020). (Closes: #414032)
+  * debian/dovecot-common.postinst: handles better chmod/chown on package
+    upgrade. (Closes: #414257)
+
+ -- Fabio Tranchitella <kobold@debian.org>  Mon, 12 Mar 2007 10:03:34 +0100
+
+dovecot (1.0.rc26-2) unstable; urgency=low
+
+  * debian/control:
+    + dovecot-{imapd,pop3d}: depends on the same-source dovecot-common.
+      (Closes: #414032)
+  * debian/rules: fixed permission for dovecot.conf. (Closes: #413995)
+
+ -- Fabio Tranchitella <kobold@debian.org>  Fri,  9 Mar 2007 12:57:50 +0100
+
+dovecot (1.0.rc26-1) unstable; urgency=low
+
+  * New upstream release.
+  * Add support for ucf, thanks to Vincent Danjean for providing a full patch.
+    (Closes: #413081)
+  * debian/dovecot-common.init: create /var/run directories at start-up time.
+    (Closes: #376143)
+
+ -- Fabio Tranchitella <kobold@debian.org>  Wed,  7 Mar 2007 11:26:56 +0100
+
+dovecot (1.0.rc25-1) unstable; urgency=low
+
+  * New upstream release.
+
+ -- Fabio Tranchitella <kobold@debian.org>  Fri,  2 Mar 2007 13:44:44 +0100
+
+dovecot (1.0.rc24-1) unstable; urgency=low
+
+  * New upstream release.
+
+ -- Fabio Tranchitella <kobold@debian.org>  Fri, 23 Feb 2007 15:47:30 +0100
+
+dovecot (1.0.rc23-1) unstable; urgency=low
+
+  * New upstream release.
+  * debian/patches/documentation.dpatch, debian/patches/xfs_quotas.dpatch:
+    removed, applied upstream.
+
+ -- Fabio Tranchitella <kobold@debian.org>  Wed, 21 Feb 2007 09:34:44 +0100
+
+dovecot (1.0.rc22-2) UNRELEASED; urgency=low
+
+  * debian/watch: added.
+
+ -- Fabio Tranchitella <kobold@debian.org>  Fri,  9 Feb 2007 12:15:34 +0100
+
+dovecot (1.0.rc22-1) unstable; urgency=low
+
+  * New upstream release.
+
+ -- Fabio Tranchitella <kobold@debian.org>  Wed,  7 Feb 2007 09:38:34 +0100
+
+dovecot (1.0.rc21-1) unstable; urgency=low
+
+  * New upstream release.
+
+ -- Fabio Tranchitella <kobold@debian.org>  Mon,  5 Feb 2007 16:23:33 +0100
+
+dovecot (1.0.rc19-1) unstable; urgency=low
+
+  * New upstream release.
+
+ -- Fabio Tranchitella <kobold@debian.org>  Wed, 24 Jan 2007 00:03:38 +0100
+
+dovecot (1.0.rc18-2) unstable; urgency=low
+
+  * dovecot-sieve: updated to cvs version 1.0.1.
+  * debian/README.Debian: updated for the new setting mail_location, which
+    substitutes the old default_mail_env. (Closes: #408025)
+
+ -- Fabio Tranchitella <kobold@debian.org>  Tue, 23 Jan 2007 10:15:36 +0100
+
+dovecot (1.0.rc18-1) unstable; urgency=low
+
+  * New upstream release.
+
+ -- Fabio Tranchitella <kobold@debian.org>  Mon, 22 Jan 2007 18:15:02 +0100
+
+dovecot (1.0.rc17-1) unstable; urgency=low
+
+  * New upstream release.
+  * Updated dovecot-sieve from CVS.
+
+ -- Fabio Tranchitella <kobold@debian.org>  Fri, 12 Jan 2007 09:42:47 +0100
+
+dovecot (1.0.rc15-2) unstable; urgency=medium
+
+  * debian/dovecot-common.README.Debian: updated details about raw logging;
+    thanks Chris Moore for providing a patch. (Closes: #400689)
+  * debian/patches/dovecot-example.dpatch: added a missing slash for an
+    absolute path. (Closes: #400830)
+  * debian/patches/dovecotpw.dpatch: applied patched to fix argument parsing
+    on some architectures. (Closes: #402075)
+
+ -- Fabio Tranchitella <kobold@debian.org>  Mon, 18 Dec 2006 18:34:31 +0100
+
+dovecot (1.0.rc15-1) unstable; urgency=medium
+
+  * New upstream release.
+  * Fixes a security bug: Off-by-one buffer overflow with mmap_disable=yes.
+    (See: http://www.dovecot.org/list/dovecot-news/2006-November/000023.html)
+
+ -- Fabio Tranchitella <kobold@debian.org>  Mon, 20 Nov 2006 12:47:39 +0100
+
+dovecot (1.0.rc14-1) unstable; urgency=medium
+
+  * New upstream release.
+
+ -- Fabio Tranchitella <kobold@debian.org>  Thu, 16 Nov 2006 09:37:38 +0100
+
+dovecot (1.0.rc13-1) unstable; urgency=medium
+
+  * New upstream release.
+  * debian/rules:
+    + preserve upstream config.guess and config.sub. (Closes: #397404)
+    + really clean dovecot-sieve/src on clean target. (Closes: #397407)
+  * debian/control: added build-conflict with automake1.4. (Closes: #397409)
+  * dovecot-sieve/src/Makefile.am: move sieve plug-ins under
+    usr/lib/dovecot/lda/modules; thanks to Chris Vanden Berghe for pointing
+    this out.
+
+ -- Fabio Tranchitella <kobold@debian.org>  Tue,  7 Nov 2006 09:26:56 +0100
+
+dovecot (1.0.rc12-1) unstable; urgency=low
+
+  * New upstream release.
+
+ -- Fabio Tranchitella <kobold@debian.org>  Sun,  5 Nov 2006 16:52:52 +0100
+
+dovecot (1.0.rc10-3) unstable; urgency=medium
+
+  * debian/rules: fixed two typos in the configure call. (Closes: #395016)
+  * Included dovecot-sieve plug-in from CVS. (Closes: #394885)
+  * Urgency medium: we are near the freeze, and this release must be part
+    of etch.
+
+ -- Fabio Tranchitella <kobold@debian.org>  Tue, 31 Oct 2006 06:30:45 +0000
+
+dovecot (1.0.rc10-2) unstable; urgency=low
+
+  * debian/patches/dovecot-example.dpatch: commented out a close brace.
+    (Closes: #394785)
+
+ -- Fabio Tranchitella <kobold@debian.org>  Mon, 23 Oct 2006 08:17:13 +0000
+
+dovecot (1.0.rc10-1) unstable; urgency=low
+
+  * New upstream release. (Closes: #393004)
+  * debian/patches/dovecot-example.dpatch:
+    + added specific comments to the mail_extra_groups option.
+      (Closes: #383453)
+    + removed duplicated LDA section. (Closes: #391632)
+  * debian/dovecot.8: fixed a layout error. (Closes: #393080)
+  * debian/dovecot-common.init: added LSB headers.
+  * Switched to the upstream dovecot deliver (LDA).
+  * debian/patches/quota_v2.dpatch: added, thanks to Jonas Smedegaard.
+    (Closes: #377563)
+
+ -- Fabio Tranchitella <kobold@debian.org>  Sun, 22 Oct 2006 08:55:16 +0000
+
+dovecot (1.0.rc7-1) unstable; urgency=low
+
+  * New upstream release. (Closes: #377840, #385101)
+  * debian/patches/dovecot-example.dpatch: set a default value for
+    pop3_uidl_format. (Closes: #383883)
+
+ -- Fabio Tranchitella <kobold@debian.org>  Tue, 29 Aug 2006 10:38:17 +0200
+
+dovecot (1.0.rc6-1) unstable; urgency=low
+
+  * New upstream release:
+    + Fixed imap segfaults on small mbox files (2 bytes). (Closes: #377840)
+    + Fixed a known bug in dovecot's IDLE handler. (Closes: #351828)
+    + Added support for quota2. (Closes: #377563)
+  * debian/control: converted build-depends on linux-kernel-headers to
+    build-conflicts to help the GNU/kFreeBSD port. (Closes: #377479)
+  * debian/control: changed maintainer to "Dovecot Maintainers"; no changes
+    to the email addresses.
+
+ -- Fabio Tranchitella <kobold@debian.org>  Tue, 15 Aug 2006 10:58:57 +0200
+
+dovecot (1.0.rc2-2) unstable; urgency=low
+
+  * patched the quota plugin to fix a missing symbol (Closes: #377018)
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Fri,  7 Jul 2006 10:50:04 -0400
+
+dovecot (1.0.rc2-1) unstable; urgency=high
+
+  * New upstream release
+  * Update dovecot-lda to the latest version (05132006)
+  * IPv6 with SSL/TLS should work now.  (Closes: #374783)
+  * go back to using poll instead of epoll.  (Closes: #376222)
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Thu,  6 Jul 2006 00:47:56 -0400
+
+dovecot (1.0.rc1-1) unstable; urgency=low
+
+  * New upstream release.
+  * Add a build-dependency on linux-kernel-headers for the xfs quotas
+    stuff.  Make it higher than the version in sarge because sarges xfs
+    includes are too old.  If any knowledgeable person would like to
+    give me a patch for this, please do.  (Closes: #374793)
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Wed, 28 Jun 2006 11:42:07 -0400
+
+dovecot (1.0.beta9-1) unstable; urgency=low
+
+  * New upstream release
+  * Added XFS quota support.  Thanks  Pawel Jarosz <pj@rsi.pl>
+    (Closes: #373936)
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Mon, 19 Jun 2006 16:55:20 -0400
+
+dovecot (1.0.beta8-4) unstable; urgency=high
+
+  * Unfortunately, the patch in the last version broke the mysql module.
+    Fixed thanks to Martin Pitt.  (Closes: #369359, #373227)
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Sun, 11 Jun 2006 16:27:43 -0400
+
+dovecot (1.0.beta8-3) unstable; urgency=high
+
+  * [SECURITY] SQL injection could occur in the postgresql module with
+    certain client character encodings. (See CVE-2006-2314)
+    Used the patch from upstream and Martin Pitt <martin.pitt@ubuntu.com>.
+    Thanks Martin.  (Closes: #369359)
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Sun, 11 Jun 2006 15:33:55 -0400
+
+dovecot (1.0.beta8-2) unstable; urgency=high
+
+  * Don't chown/chmod ssl certificate unless we created it.
+    (Closes: #364766)
+  * Upstream fixed the crash if passwd-file had entries without passwords.
+    (Closes: #361536)
+  * fixed up the last versions changelog to better describe the security
+    problem which was fixed there.
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Sun, 21 May 2006 13:16:17 -0400
+
+dovecot (1.0.beta8-1) unstable; urgency=high
+
+  * New upstream release.
+  * [SECURITY] Fixes a directory traversal vulnerability.
+    (see: http://www.dovecot.org/list/dovecot-news/2006-May/000006.html
+    and CVE-2006-2414)
+  * Set urgency to high: this version fixes a security bug
+  * Standards-Version: 3.7.2, no changes needed.
+
+ -- Fabio Tranchitella <kobold@debian.org>  Sat, 13 May 2006 22:46:16 +0200
+
+dovecot (1.0.beta7-1) unstable; urgency=low
+
+  * New upstream version.
+  * Added sqlite support.
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Wed, 12 Apr 2006 23:25:41 -0400
+
+dovecot (1.0.beta5-1) unstable; urgency=low
+
+  * New upstream version.  Also updated dovecot-lda from CVS.
+  * debian/control.  Added build-depends on flex to prevent FTBFS.
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Thu,  6 Apr 2006 16:22:46 -0400
+
+dovecot (1.0.beta3-3) unstable; urgency=low
+
+  * Compile against the newer mysql library. (Closes: #356729)
+
+ -- Fabio Tranchitella <kobold@debian.org>  Wed, 22 Mar 2006 13:43:04 +0000
+
+dovecot (1.0.beta3-2) unstable; urgency=low
+
+  [Fabio Tranchitella]
+  * debian/control: added build-depends on byacc.
+  * debian/rules: removed --with-vpopmail option, because libvpopmail-dev
+    is in contrib and we don't wanto to have dovecot build-depends on it.
+  * debian/patches/dovecot-example.dpatch: added two small commented
+    block of configuration for dovecot-lda.
+
+ -- Fabio Tranchitella <kobold@debian.org>  Sun, 26 Feb 2006 20:59:06 +0000
+
+dovecot (1.0.beta3-1) unstable; urgency=high
+
+  [Fabio Tranchitella]
+  * New upstream release, which fixes two security related bugs.
+    CVE-2006-0730 (Closes: #353341)
+  * Included dovecot-lda (ver. 20060209). (Closes: #353307, #347348, #333962)
+
+  [ Jaldhar H. Vyas ]
+  * Removed the code for upgrading impad.pem.  This might bite if you if
+    you try and upgrade a woody version of dovecot to this one.  So
+    don't do that.  (Closes: #337715)
+  * dovecot-imapd,dovecot-pop3d: depend on dovecot-common >= 1.0beta3-1
+    as the way SSL parameters are generated has changed.  (Closes: #353404)
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Fri, 17 Feb 2006 22:06:21 -0500
+
+dovecot (1.0.beta2-1) unstable; urgency=low
+
+  [Fabio Tranchitella]
+  * New upstream release.
+  * debian/rules: compile with vpopmail support. (Closes: #347838)
+  * debian/patches: removed zlib patch (merged with upstream).
+
+ -- Fabio Tranchitella <kobold@debian.org>  Thu,  2 Feb 2006 21:38:32 +0000
+
+dovecot (1.0.alpha5-1) unstable; urgency=low
+
+  [Fabio Tranchitella]
+  * New upstream release.
+  * Compile dovecot with Kerberos support. (Closes: #338384)
+  * Fixed a small typo in mbox specification. (Closes: #339789)
+  * Added man page for maildirmake.dovecot, thanks to Henry Precheur.
+    (Closes: #340498)
+  * Use /usr/lib/dovecot/modules as basedir for dynamic modules.
+    Upstream suggests /usr/lib/dovecot, but we already use it as
+    libexec directory.
+
+ -- Fabio Tranchitella <kobold@debian.org>  Wed, 21 Dec 2005 13:44:38 +0000
+
+dovecot (1.0.alpha4-1) unstable; urgency=low
+
+  [Jaldhar H. Vyas]
+  * New upstream version.
+  * Made sure the default dovecot.conf includes mail_extra_groups=mail
+    (Closes: #336476) somehow this edit got lost at some point.
+  * use ISO8601 date format as default value for log_timestamp in
+    /etc/dovecot/dovecot.conf  (Closes: #333059)
+  * stop shipping {arch} directories in source (Closes: #334646)
+  * Include plugin for compressed mboxen (Closes: #332384)
+  * updated NEWS.Debian to warn users that the dovecot.conf syntax has
+    changed (Closes: #334209)
+  * Remember, remember, the 5th of November.
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Sat,  5 Nov 2005 23:19:19 -0500
+
+dovecot (1.0.alpha3-2) unstable; urgency=low
+
+  [Jaldhar H. Vyas]
+  * dovecot-common: When creating the dovecot user in the postinst,
+    the --ingroup option to adduser to add dovecot to group
+    mail isn't used anymore.
+    (Closes: #330960, #331106)
+  * commented out userdb passdb from default configuration.  Most
+    people won't need that. (Closes: #330978)
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Thu,  6 Oct 2005 14:25:33 -0400
+
+dovecot (1.0.alpha3-1) unstable; urgency=low
+
+  [ Fabio Tranchitella ]
+  * New upstream release (dovecot-1.0.alpha.3)
+  * debian/patches/ipv6_v6only.dpatch: removed, upstream accepted it.
+  * debian/dovecot-common.postinst: removed bashisms.
+  * debian/dovecot-common.postinst: add dovecot user to group mail.
+    (Closes: #323921)
+  * debian/control: removed conflicts with imap-server and pop3-server,
+    added replaces instead. (Closes: #324480)
+  [ Jaldhar H. Vyas ]
+  * No longer crashes when using LDAP as userdb/passdb (Closes: #320388)
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Mon, 26 Sep 2005 01:42:09 -0400
+
+dovecot (0.99.20050712-2) unstable; urgency=low
+
+  * Fabio Tranchitella <kobold@debian.org>
+    + debian/control: dovecot-common has to depend on adduser.
+    + debian/patches/documentation.dpatch: some cosmetic fixes about mysql
+      backend.
+  * Jaldhar H. Vyas <jaldhar@debian.org>
+    + debian/control: tighten dovecot-imapd and dovecot-pop3d's dependency on
+      dovecot-common (Closes: #319465)
+    + debian/patches/dovecot-example.dpatch: some more fixes to default
+      configuration.  (Closes: #319413, #319941)
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Fri, 29 Jul 2005 15:37:52 -0400
+
+dovecot (0.99.20050712-1) unstable; urgency=low
+
+  * Fabio Tranchitella <kobold@debian.org>
+    + New upstream version (dovecot-stable, last update 20050712).
+      (Closes: #312893)
+    + debian/control: Standards-Version: 3.6.2 (no changes needed).
+    + debian/patches/dovecot-sql.dpatch: use the right path for mysql socket.
+      (Closes: #298874)
+  * Jaldhar H. Vyas <jaldhar@debian.org>
+    + Removed dovecot package as it was just a woody->sarge transitional
+      pseudo-package.
+    + Apply patch to debian/dovecot-common.init to help when manually
+      starting dovecot.  Thanks Roland Stigge.  (Closes: #309679)
+    + Apply patch to src/lib/network.c to support IPV6_V6ONLY.  Thanks
+      Marco D'Itri. (Closes: #308652)
+    + depend on the latest postgresql library.
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Wed, 20 Jul 2005 06:30:37 -0400
+
+dovecot (0.99.14-1) unstable; urgency=low
+
+  * New upstream version.
+  * dovecot-common: another postinst regexp fix for SSL cert/key files.
+    (Closes: #294989)
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Sat, 12 Feb 2005 21:34:33 -0500
+
+dovecot (0.99.13-6) unstable; urgency=high
+
+  * dovecot-common: *sigh* another init script fix.  Hopefully we now
+    fully deal with dovecot being run from inetd.  Thanks again to Magnus
+    Holmgren.  (Closes: #293348)
+  * High again so -5 doesn't get into sarge.
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Mon,  7 Feb 2005 02:58:30 -0500
+
+dovecot (0.99.13-5) unstable; urgency=high
+
+  * dovecot-common: typo in postinst resulted in incorrect generation of
+    keys for first-time installers.  Hence urgency high.
+  * dovecot-common: In init script, make extra check to make sure an
+    IMAP or POP3 server called from inetd is dovecot and not some other
+    random inferior product.  (Closes: #293348)
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Sat,  5 Feb 2005 13:56:31 -0500
+
+dovecot (0.99.13-4) unstable; urgency=low
+
+  * build depend on libmysqlclient12
+  * dovecot-common: Allow STARTTLS to work when dovecot is run from inetd
+    Thanks Magnus Holmgren (Closes: #290985)
+  * dovecot-common: let init script exit if dovecot is being run from inetd
+    Thanks Magnus Holmgren (Closes: #292195)
+  * dovecot-common: fix a number of problems in postinst
+    + fails if /etc/ssl/certs or /etc/ssl/private doesn't exist
+    + certs cannot be generated and upgrade fails if openssl is not
+      configured.  Fail more gracefully if this is the case.
+    + read the name and path for the cert from dovecot.conf instead of
+      hardcoding it.
+      Thanks Frederic Pauget (Closes: #292344)
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Sun, 30 Jan 2005 15:20:03 -0500
+
+dovecot (0.99.13-3) unstable; urgency=high
+
+  * Oops -2 had to be urgency=high so -1 doesn't get into sarge.
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Sat,  8 Jan 2005 12:11:38 -0500
+
+dovecot (0.99.13-2) unstable; urgency=low
+
+  * dovecot-imapd, dovecot-pop3d:  It occurred to me that the effects of
+    fixing #288391 will cause confusion in the minds of new installers so I
+    should add a warning in README.Debian and NEWS.Debian in a vain
+    effort to stave off swarms of bug reports.  (Vain, because no one
+    actually reads documentation anyway.)
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Sat,  8 Jan 2005 11:29:59 -0500
+
+dovecot (0.99.13-1) unstable; urgency=high
+
+  * New upstream version.
+  * dovecot-imapd, dovecot-pop3d: No longer mess with dovecot.conf in postinst
+    (Closes: #288391)
+  * urgency high due to #288391 being a release-critical bug.
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Fri,  7 Jan 2005 17:37:08 -0500
+
+dovecot (0.99.12-1) unstable; urgency=low
+
+  * New upstream version.  (Yes I know 0.99.13 is just around the corner.)
+  * SASL is re-enabled so this bug ("Dovecot seems not to require SASL")
+    is no longer valid (Closes: #272093)
+  * Configuration files moved to /etc/dovecot (Closes: #276183)
+  * Permissions on /var/run/dovecot and /var/run/dovecot/login no longer
+    give warnings. (Closes: #283996)
+  * SSL certificate is world readable (Closes: #277114).
+  * Thanks to Jan Buren, much extra documentation has been added to
+    /usr/share/doc/dovecot-common/README.Debian
+  * Lintian overrides added.
+  * Happy new year to all you Gregorians
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Fri, 31 Dec 2004 15:55:07 -0500
+
+dovecot (0.99.11-3) unstable; urgency=medium
+
+  * applied dovecot-large-header-fix patch to prevent 100% CPU
+    utilization when dealing with really large headers.
+    (Closes: #271458)
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Mon, 13 Sep 2004 20:20:23 -0400
+
+dovecot (0.99.11-2) unstable; urgency=low
+
+  * Eliminated duplicate stanza in dovecot.conf (Closes: #270181)
+  * Reapplied CRAM-MD5 patch.
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Mon,  6 Sep 2004 01:24:53 -0400
+
+dovecot (0.99.11-1) unstable; urgency=low
+
+  * New upstream release.
+  * patch to give bug reporting address in configure.ac.
+    Thanks Matthias Andree.
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Sat,  4 Sep 2004 14:24:57 -0400
+
+dovecot (0.99.10.9-2) unstable; urgency=low
+
+  * screw mipsel.
+  * Added PAM_RHOST patch.  Thanks Dean Gaudet.  (Closes: #264712)
+  * Added CRAM-MD5 patch.  Thanks Joshua Goodall.
+  * Added unexpected EOF patch from Timo.
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Tue, 17 Aug 2004 01:13:20 -0400
+
+dovecot (0.99.10.9-1) unstable; urgency=low
+
+  * New upstream release.
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Mon,  2 Aug 2004 18:56:02 -0400
+
+dovecot (0.99.10.8-1) unstable; urgency=low
+
+  * New upstream release.
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Fri, 30 Jul 2004 08:17:51 -0400
+
+dovecot (0.99.10.7-1) unstable; urgency=low
+
+  * New upstream release.
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Wed, 14 Jul 2004 07:29:49 -0400
+
+dovecot (0.99.10.6-3) unstable; urgency=low
+
+  * Patched so dovecot follows symlinks to directories again.
+    (Closes: #256061)
+  * Changed the priority of init script so it is run after postgresql
+    (Closes: #256068)
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Thu, 24 Jun 2004 23:57:50 -0400
+
+dovecot (0.99.10.6-2) unstable; urgency=high
+
+  * I needed to enable one more parameter in the configuration in order
+    to get dot-locking working.  Hence this should still be high urgency.
+    (Really Closes: #185335)
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Mon, 21 Jun 2004 20:02:29 -0400
+
+dovecot (0.99.10.6-1) unstable; urgency=high
+
+  * New upstream version.
+    + finally fixes dot-locking so I think it deserves high priority for
+      sarge.  (Closes: #185335)
+  * dovecot: fixed a typo in description (Closes: #254415)
+  * dovecot-common: man page for dovecot added.  Thanks Kai Hendry.
+    (Closes: #253482)
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Sat, 19 Jun 2004 23:18:39 -0400
+
+dovecot (0.99.10.5-4) unstable; urgency=high
+
+  * Crap, typo in dovecot-common.postinst sorry.  This should only
+    affect new installs though.
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Fri, 11 Jun 2004 22:30:41 -0400
+
+dovecot (0.99.10.5-3) unstable; urgency=high
+
+  * SECURITY:  set permissions on config files to 0600 to prevent
+    disclosure of sensitive information to local users. (Closes: #253760)
+  * SECURITY: Tightened permissions on generated SSL certificate.
+    (Closes: #253833)
+  * Made a note that dovecot-openssl.conf is not needed on Debian
+    because we generate a certificate in dovecot-commons' postinst.
+    (Closes: #253774)
+  * Added maildir-autocreate patch.
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Fri, 11 Jun 2004 09:12:07 -0400
+
+dovecot (0.99.10.5-2) unstable; urgency=low
+
+  * Added maildir-stat patch.
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Thu, 10 Jun 2004 14:39:28 -0400
+
+dovecot (0.99.10.5-1) unstable; urgency=low
+
+  * New upstream version.
+  * Enabled mysql support.
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Thu, 27 May 2004 13:21:42 -0400
+
+dovecot (0.99.10.4-5) unstable; urgency=high
+
+  * Switched to using openssl as dovecot segfaults with gnutls7 and is
+    not compatible with gnutls10 (Closes: #244570)
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Mon, 19 Apr 2004 22:45:21 -0400
+
+dovecot (0.99.10.4-4) unstable; urgency=low
+
+  * Added a patch to src/auth/db-pgsql.c from Zsolt VARGA.
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Tue, 30 Mar 2004 12:49:31 -0500
+
+dovecot (0.99.10.4-3) unstable; urgency=high
+
+  * dovecot-common: Fix postinst to no longer delete /etc/pam.d/imap
+    (Closes: #232832)
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Mon, 15 Mar 2004 10:27:52 -0500
+
+dovecot (0.99.10.4-2) unstable; urgency=low
+
+  * dovecot-common: now replaces: dovecot for smoother upgrades.
+    (Closes: #223666)
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Thu, 11 Dec 2003 09:18:12 -0500
+
+dovecot (0.99.10.4-1) unstable; urgency=low
+
+  * New upstream version
+    + This fixes the corruption of .subscriptions files with folders in
+      maildir format. (Closes: #222272)
+  * Some extra information included in dovecot-common.README.Debian.
+    (Closes: #221106)  There should probably be more so if you have ideas
+    let me know.
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Mon,  1 Dec 2003 23:41:00 -0500
+
+dovecot (0.99.10.2-1) unstable; urgency=low
+
+  * New upstream version.
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Tue, 11 Nov 2003 17:21:45 -0500
+
+dovecot (0.99.10-11) unstable; urgency=low
+
+  * Build depend on gnutls 7 instead of 5 (Closes: #219523)
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Thu,  6 Nov 2003 22:25:00 -0500
+
+dovecot (0.99.10-10) unstable; urgency=low
+
+  * maildirmake.dovecot will now let you create maildirs whose names
+   have spaces in them and chown them to a specified user.  Thanks Paul
+   Slootman (Closes: #219168)
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Tue,  4 Nov 2003 20:00:25 +0000
+
+dovecot (0.99.10-9) unstable; urgency=low
+
+  * Don't use SASL2 as upstream says the support is broken.
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Tue, 23 Sep 2003 16:27:11 +0000
+
+dovecot (0.99.10-8) unstable; urgency=low
+
+  * Patched so suid works on 2.6 kernels.  Thanks Peter Gervai.
+    (Closes: #211420)
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Wed, 17 Sep 2003 17:59:10 +0000
+
+dovecot (0.99.10-7) unstable; urgency=low
+
+  * Yet another init script fix.  It should be ok now.  Thanks once again
+    to Alexis Iglauer.
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Wed, 27 Aug 2003 10:56:04 -0400
+
+dovecot (0.99.10-6) unstable; urgency=low
+
+  * fix some errors in init script (closes: #207464)
+    Thanks to Adam Lackorzynski and Alexis Iglauer.
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Wed, 27 Aug 2003 09:02:23 -0400
+
+dovecot (0.99.10-5) unstable; urgency=high
+
+  * dovecot-pop3d, dovecot-imapd: make sure init script doesn't
+    attempt to start the daemons if unconfigured thus preventing
+    segfault on startup.  (Closes: #206992, #207140)
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Mon, 25 Aug 2003 16:40:53 -0400
+
+dovecot (0.99.10-4) unstable; urgency=low
+
+  * Updated PAM configuration to the new scheme and added appropriate
+    dependency.
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Fri, 22 Aug 2003 12:54:54 -0400
+
+dovecot (0.99.10-3) unstable; urgency=low
+
+  * dovecot-pop3d: Patch for proper PAM service name.
+  * dovecot-imapd, dovecot-pop3d: Make sure there is an appropriate
+    entry in the protocol = line in /etc/dovecot.conf so the service
+    will start up without errors. (Closes: #204213)
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Thu, 21 Aug 2003 13:47:00 -0400
+
+dovecot (0.99.10-2.1) unstable; urgency=low
+
+  * Non-maintainer upload at request of maintainer.
+  * Fix segfault on alpha caused by time_t size. Closes: #203892.
+  * Fix segfault when user's home directory is left empty.
+
+ -- Scott James Remnant <scott@netsplit.com>  Wed,  6 Aug 2003 01:47:16 +0100
+
+dovecot (0.99.10-2) unstable; urgency=low
+
+  * corrected paths to example and configuration files in sample config
+    (Closes: #199740)
+  * Added postgresql support.
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Thu,  3 Jul 2003 16:45:49 -0400
+
+dovecot (0.99.10-1) unstable; urgency=low
+
+  * New upstream release.
+  * PAM service name has changed to dovecot (for IMAP and POP3.)  I've included
+    code to move /etc/pam.d/imap to /etc/pam.d/dovecot but if things suddenly
+    stop working, this is the first thing to check.
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Thu, 26 Jun 2003 22:31:07 -0400
+
+dovecot (0.99.10-0.rc2) unstable; urgency=low
+
+  * New upstream release.  Fixes broken imaps support.
+  * Typo in configure options that broke LDAP support on woody corrected.
+  * Only start /usr/sbin/dovecot if either the IMAP or POP3 servers are
+    installed. (Closes: #192066)
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Mon, 23 Jun 2003 23:26:04 -0400
+
+dovecot (0.99.9.1-1) unstable; urgency=low
+
+  * New upstream release.
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Sun,  4 May 2003 21:49:55 -0400
+
+dovecot (0.99.9-1) unstable; urgency=low
+
+  * New upstream release.
+  * The IMAP and POP3 servers have been split into separate package so you
+    don't have to install both.  There is also a dovecot-common package
+    for the parts they share.  The dovecot package is now a dummy just for
+    transitioning to this new scheme.  (Closes: #187826)
+  * Allow chmod in maildirmake.dovecot to fail gracefully (Closes: #191244)
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Wed, 30 Apr 2003 08:53:46 -0400
+
+dovecot (0.99.8.1-4) unstable; urgency=low
+
+  * Added a build-depends on libsasl-dev (Closes: #187516)
+  * Enabled pop3 service.  However it is still turned off in the config file by
+    default so as to not surprise anyone who thought they only had an IMAP
+    server.
+  * Consequently, changed "IMAP server" in descriptions etc. to "mail server".
+  * only skip key generation if both /etc/ssl/certs/dovecot.pem and
+    /etc/ssl/private/dovecot.pem exist.  (Closes: #187638)
+  * post-0.99.8.1 patch: Fix renaming subfolders with maildir.
+  * post-0.99.8.1 patch: Fix other maildir subfolder problems.
+  * post-0.99.8.1 patch: Fix partial body fetches.
+  * post-0.99.8.1 patch: Fix using LITERAL+APPEND.
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Sat,  5 Apr 2003 14:13:52 -0500
+
+dovecot (0.99.8.1-3) unstable; urgency=low
+
+  * Fixed bashism and perlism(!) in maildirmake.dovecot.  Thanks Clint
+    Adams. (Closes: #185768)
+  * Enabled LDAP support.  The configuration is in /etc/dovecot-ldap.conf
+    but is commented out.
+  * Happy new year to all Debian users still on the Julian calendar.
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Tue, 25 Mar 2003 09:25:37 -0500
+
+dovecot (0.99.8.1-2) unstable; urgency=low
+
+  * Make a separate check for /etc/ssl/private/imapd.pem in case we don't
+    have it. (Closes: #185334)
+  * Move check for dovecot user and creation code to postinsts' configure
+    phase. (Closes: #185333)
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Wed, 19 Mar 2003 01:50:10 -0500
+
+dovecot (0.99.8.1-1) unstable; urgency=low
+
+  * new upstream release.  (Closes: #184131)  Does not include the POP3
+    server or LDAP and SASL support for now so we can get into the archive
+    quickly.  Fixes segfaults in imap-master (Closes: #184231)  Fixes
+    faulty mail autodetection presets (Closes: #179625)
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Sun, 16 Mar 2003 15:47:54 -0500
+
+dovecot (0.99.7-4) unstable; urgency=low
+
+  * Fixed location of handlers. (Closes: #179273)
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Fri, 31 Jan 2003 18:37:23 -0500
+
+dovecot (0.99.7-3) unstable; urgency=low
+
+  * doh pam file should be called imap not dovecot (Closes: #179180)
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Fri, 31 Jan 2003 14:12:29 -0500
+
+dovecot (0.99.7-2) unstable; urgency=low
+
+  * Added two upstream patches to fix broken plain authentication and
+    building with vpopmail support (which is not enabled in the Debian
+    package yet.)
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Fri, 17 Jan 2003 14:24:22 -0500
+
+dovecot (0.99.7-1) unstable; urgency=low
+
+  * New upstream release.
+  * Syslog no longer fills up with entries when dovecot is misconfigured.
+    (Closes: #175507)
+  * startup is logged now. shutdowns were already logged. (Closes: #175509)
+  * dovecot will not remove your SSL certificates now.  (Closes: #175282)
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Wed, 15 Jan 2003 00:58:23 -0500
+
+dovecot (0.99.6rc2-1) unstable; urgency=low
+
+  * New upstream release.
+  * Includes patch which stops dovecot from dying on alpha (Closes: #175577)
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Wed,  8 Jan 2003 14:22:09 -0500
+
+dovecot (0.99.5-1) unstable; urgency=low
+
+  * New upstream release with many fixes and improvements.
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Fri,  3 Jan 2003 00:53:26 -0500
+
+dovecot (0.99.4-1) unstable; urgency=low
+
+  * Initial Release.
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Wed, 18 Dec 2002 09:44:39 -0500
diff --git a/conf/conf.d/10-auth.conf b/conf/conf.d/10-auth.conf
new file mode 100644 (file)
index 0000000..edd148c
--- /dev/null
@@ -0,0 +1,120 @@
+##
+## Authentication processes
+##
+
+# Enable LOGIN command and all other plaintext authentications even if
+# SSL/TLS is not used (LOGINDISABLED capability). Note that if the remote IP
+# matches the local IP (ie. you're connecting from the same computer), the
+# connection is considered secure and plaintext authentication is allowed,
+# unless ssl = required.
+#auth_allow_cleartext = yes
+
+# Authentication cache size (e.g. 10M). 0 means it's disabled. Note that
+# bsdauth, PAM and vpopmail require cache_key to be set for caching to be used.
+#auth_cache_size = 0
+# Time to live for cached data. After TTL expires the cached record is no
+# longer used, *except* if the main database lookup returns internal failure.
+# We also try to handle password changes automatically: If user's previous
+# authentication was successful, but this one wasn't, the cache isn't used.
+# For now this works only with plaintext authentication.
+#auth_cache_ttl = 1 hour
+# TTL for negative hits (user not found, password mismatch).
+# 0 disables caching them completely.
+#auth_cache_negative_ttl = 1 hour
+
+# Space separated list of realms for SASL authentication mechanisms that need
+# them. You can leave it empty if you don't want to support multiple realms.
+# Many clients simply use the first one listed here, so keep the default realm
+# first.
+#auth_realms =
+#
+# Default realm/domain to use if none was specified. This is used for both
+# SASL realms and appending @domain to username in plaintext logins.
+#auth_default_domain =
+
+# List of allowed characters in username. If the user-given username contains
+# a character not listed in here, the login automatically fails. This is just
+# an extra check to make sure user can't exploit any potential quote escaping
+# vulnerabilities with SQL/LDAP databases. If you want to allow all characters,
+# set this value to empty.
+#auth_username_chars = abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890.-_@
+
+# Username character translations before it's looked up from databases. The
+# value contains series of from -> to characters. For example "#@/@" means
+# that '#' and '/' characters are translated to '@'.
+#auth_username_translation =
+
+# Username formatting before it's looked up from databases.
+#auth_username_format = %{user|lower}
+#auth_username_format = %{user|username|lower}
+
+# If you want to allow master users to log in by specifying the master
+# username within the normal username string (ie. not using SASL mechanism's
+# support for it), you can specify the separator character here. The format
+# is then <username><separator><master username>. UW-IMAP uses "*" as the
+# separator, so that could be a good choice.
+#auth_master_user_separator =
+
+# Username to use for users logging in with ANONYMOUS SASL mechanism
+#auth_anonymous_username = anonymous
+
+# Host name to use in GSSAPI principal names. The default is to use the
+# name returned by gethostname(). Use "$ALL" (with quotes) to allow all keytab
+# entries.
+#auth_gssapi_hostname =
+
+# Kerberos keytab to use for the GSSAPI mechanism. Will use the system
+# default (usually /etc/krb5.keytab) if not specified. You may need to change
+# the auth service to run as root to be able to read this file.
+#auth_krb5_keytab = 
+
+# Do NTLM and GSS-SPNEGO authentication using Samba's winbind daemon and
+# ntlm_auth helper. <https://doc.dovecot.org/latest/core/config/auth/mechanisms/winbind.html>
+#auth_use_winbind = no
+
+# Path for Samba's ntlm_auth helper binary.
+#auth_winbind_helper_path = /usr/bin/ntlm_auth
+
+# Time to delay before replying to failed authentications.
+#auth_failure_delay = 2 secs
+
+# Require a valid SSL client certificate or the authentication fails.
+#auth_ssl_require_client_cert = no
+
+# Take the username from client's SSL certificate, using 
+# X509_NAME_get_text_by_NID() which returns the subject's DN's
+# CommonName. 
+#auth_ssl_username_from_cert = no
+
+# Space separated list of wanted authentication mechanisms:
+#   plain login digest-md5 cram-md5 ntlm anonymous gssapi
+#   gss-spnego xoauth2 oauthbearer
+# NOTE: See also auth_allow_cleartext setting.
+#auth_mechanisms = plain login 
+
+##
+## Password and user databases
+##
+
+#
+# Password database is used to verify user's password (and nothing more).
+# You can have multiple passdbs and userdbs. This is useful if you want to
+# allow both system users (/etc/passwd) and virtual users to login without
+# duplicating the system users into virtual database.
+#
+# <https://doc.dovecot.org/latest/core/config/auth/passdb.html>
+#
+# User database specifies where mails are located and what user/group IDs
+# own them. For single-UID configuration use "static" userdb.
+#
+# <https://doc.dovecot.org/latest/core/config/auth/userdb.html>
+
+#!include auth-deny.conf.ext
+#!include auth-master.conf.ext
+#!include auth-oauth2.conf.ext
+
+!include auth-system.conf.ext
+#!include auth-sql.conf.ext
+#!include auth-ldap.conf.ext
+#!include auth-passwdfile.conf.ext
+#!include auth-static.conf.ext
diff --git a/conf/conf.d/10-logging.conf b/conf/conf.d/10-logging.conf
new file mode 100644 (file)
index 0000000..359642f
--- /dev/null
@@ -0,0 +1,86 @@
+##
+## Log destination.
+##
+
+# Log file to use for error messages. "syslog" logs to syslog,
+# /dev/stderr logs to stderr.
+#log_path = syslog
+
+# Log file to use for informational messages. Defaults to log_path.
+#info_log_path = 
+# Log file to use for debug messages. Defaults to info_log_path.
+#debug_log_path = 
+
+# Syslog facility to use if you're logging to syslog. Usually if you don't
+# want to use "mail", you'll use local0..local7. Also other standard
+# facilities are supported.
+#syslog_facility = mail
+
+##
+## Logging verbosity and debugging.
+##
+
+# Log unsuccessful authentication attempts and the reasons why they failed.
+#auth_verbose = yes
+
+# In case of password mismatches, log the attempted password. Valid values are
+# no, plain and sha1. sha1 can be useful for detecting brute force password
+# attempts vs. user simply trying the same password over and over again.
+#auth_verbose_passwords = no
+
+# To chain multiple logging conditions you can use,
+# log_debug=$SET:log_debug or category=xxx
+
+# Even more verbose logging for debugging purposes. Shows for example SQL
+# queries.
+#log_debug=category=auth
+#
+# In case of password mismatches, log the passwords and used scheme so the
+# problem can be debugged. Enabling this also enables auth_debug.
+#auth_debug_passwords = yes
+
+# Enable mail process debugging. This can help you figure out why Dovecot
+# isn't finding your mails.
+#log_debug=category=mail
+
+# Show protocol level SSL errors.
+#log_debug=category=ssl
+
+# mail_log plugin provides more event logging for mail processes.
+#mail_plugins {
+#   notify = yes
+#   mail_log = yes
+#}
+# Events to log. Also available: flag_change append
+#mail_log_events = delete undelete expunge copy mailbox_delete mailbox_rename flag_change append
+# Available fields: uid, box, msgid, from, subject, size, vsize, flags
+# size and vsize are available only for expunge and copy events.
+#mail_log_fields = uid box msgid size from subject vsize flags
+# only log cached fields
+#mail_log_cached_only = yes
+
+##
+## Log formatting.
+##
+
+# Prefix for each line written to log file. % codes are in strftime(3)
+# format.
+#log_timestamp = "%b %d %H:%M:%S "
+
+# Space-separated list of elements we want to log. The elements which have
+# a non-empty variable value are joined together to form a comma-separated
+# string.
+#login_log_format_elements = user=<%{user}> method=%{mechanism} rip=%{remote_ip} lip=%{local_ip} mpid=%{mail_pid} %{secured} session=<%{session}>
+
+# Login log format. %{elements} contains login_log_format_elements string, %{message} contains
+# the data we want to log.
+#login_log_format = %{message}: %{elements}
+ 
+# Log prefix for mail processes. See
+# https://doc.dovecot.org/latest/core/settings/variables.html#mail-service-user-variables
+# for list of possible variables.
+#mail_log_prefix = "%{service}(%{user})<%{process:pid}><%{session}>: "
+
+# Format to use for logging mail deliveries. See https://doc.dovecot.org/latest/core/summaries/settings.html#deliver_log_format
+# for list of possible variables.
+#deliver_log_format = msgid=%{msgid}: %{message} (subject=%{subject} from=%{from} size=%{size})
diff --git a/conf/conf.d/10-mail.conf b/conf/conf.d/10-mail.conf
new file mode 100644 (file)
index 0000000..31938b4
--- /dev/null
@@ -0,0 +1,410 @@
+##
+## Mailbox locations and namespaces
+##
+
+# Location for users' mailboxes. The default is empty, which means that Dovecot
+# tries to find the mailboxes automatically. This won't work if the user
+# doesn't yet have any mail, so you should explicitly tell Dovecot the full
+# location.
+#
+# If you're using mbox, giving a path to the INBOX file (eg. /var/mail/%u)
+# isn't enough. You'll also need to tell Dovecot where the other mailboxes are
+# kept. This is called the "root mail directory", and it must be the first
+# path given in the mail_location setting.
+#
+# There are a few special variables you can use, eg.:
+#
+#   %{user} - username
+#   %{user|username} - user part in user@domain, same as %u if there's no domain
+#   %{user|domain} - domain part in user@domain, empty if there's no domain
+#   %{home} - home directory
+#
+# See https://doc.dovecot.org/latest/core/settings/variables.html for full list
+# of variables.
+#
+# Example:
+#  mail_driver = maildir
+#  mail_path = ~/Maildir
+#  mail_inbox_path = ~/Maildir/.INBOX
+#
+
+# Debian defaults
+# Note that upstream considers mbox deprecated and strongly recommends
+# against its use in production environments. See further information
+# at
+# https://doc.dovecot.org/2.4.1/core/config/mailbox/formats/mbox.html
+mail_driver = mbox
+mail_home = /home/%{user | username}
+mail_path = %{home}/mail
+mail_inbox_path = /var/mail/%{user}
+
+# If you need to set multiple mailbox locations or want to change default
+# namespace settings, you can do it by defining namespace sections.
+#
+# You can have private, shared and public namespaces. Private namespaces
+# are for user's personal mails. Shared namespaces are for accessing other
+# users' mailboxes that have been shared. Public namespaces are for shared
+# mailboxes that are managed by sysadmin. If you create any shared or public
+# namespaces you'll typically want to enable ACL plugin also, otherwise all
+# users can access all the shared mailboxes, assuming they have permissions
+# on filesystem level to do so.
+namespace inbox {
+  # Namespace type: private, shared or public
+  #type = private
+
+  # Hierarchy separator to use. You should use the same separator for all
+  # namespaces or some clients get confused. '/' is usually a good one.
+  # The default however depends on the underlying mail storage format.
+  #separator = 
+
+  # Prefix required to access this namespace. This needs to be different for
+  # all namespaces. For example "Public/".
+  #prefix = 
+
+  # Physical location of the mailbox. This is in same format as
+  # mail location, which is also the default for it.
+  # mail_driver = 
+  # mail_path = 
+  #
+  # There can be only one INBOX, and this setting defines which namespace
+  # has it.
+  inbox = yes
+
+  # If namespace is hidden, it's not advertised to clients via NAMESPACE
+  # extension. You'll most likely also want to set list=no. This is mostly
+  # useful when converting from another server with different namespaces which
+  # you want to deprecate but still keep working. For example you can create
+  # hidden namespaces with prefixes "~/mail/", "~%u/mail/" and "mail/".
+  #hidden = no
+
+  # Show the mailboxes under this namespace with LIST command. This makes the
+  # namespace visible for clients that don't support NAMESPACE extension.
+  # "children" value lists child mailboxes, but hides the namespace prefix.
+  #list = yes
+
+  # Namespace handles its own subscriptions. If set to "no", the parent
+  # namespace handles them (empty prefix should always have this as "yes")
+  #subscriptions = yes
+
+  # See 15-mailboxes.conf for definitions of special mailboxes.
+}
+
+# Example shared namespace configuration
+#namespace shared {
+  #type = shared
+  #separator = /
+
+  # Mailboxes are visible under "shared/user@domain/"
+  # $user, $domain and $username are expanded to the destination user.
+  #prefix = shared/$user/
+
+  # Mail location for other users' mailboxes. Note that %{variables} and ~/
+  # expands to the logged in user's data. %{owner_user} and %{owner_home}
+  # destination user's data.
+  #mail_driver = maildir
+  #mail_path = %{owner_home}/Maildir
+  #mail_index_path = ~/Maildir/shared/%{owner_user}
+
+  # Use the default namespace for saving subscriptions.
+  #subscriptions = no
+
+  # List the shared/ namespace only if there are visible shared mailboxes.
+  #list = children
+#}
+# Should shared INBOX be visible as "shared/user" or "shared/user/INBOX"?
+#mail_shared_explicit_inbox = no
+
+# System user and group used to access mails. If you use multiple, userdb
+# can override these by returning uid or gid fields. You can use either numbers
+# or names. <https://doc.dovecot.org/latest/core/config/system_users.html#uids>
+#mail_uid =
+#mail_gid =
+
+# Group to enable temporarily for privileged operations. Currently this is
+# used only with INBOX when either its initial creation or dotlocking fails.
+# Typically this is set to "mail" to give access to /var/mail.
+mail_privileged_group = mail
+
+# Grant access to these supplementary groups for mail processes. Typically
+# these are used to set up access to shared mailboxes. Note that it may be
+# dangerous to set these if users can create symlinks (e.g. if "mail" group is
+# set here, ln -s /var/mail ~/mail/var could allow a user to delete others'
+# mailboxes, or ln -s /secret/shared/box ~/mail/mybox would allow reading it).
+#mail_access_groups =
+
+# Allow full filesystem access to clients. There's no access checks other than
+# what the operating system does for the active UID/GID. It works with both
+# maildir and mboxes, allowing you to prefix mailboxes names with eg. /path/
+# or ~user/.
+#mail_full_filesystem_access = no
+
+# Dictionary for key=value mailbox attributes. This is used for example by
+# URLAUTH and METADATA extensions.
+#mail_attribute {
+#  dict file {
+#    path = %{home}/Maildir/dovecot-attributes
+#  }
+#}
+
+# A comment or note that is associated with the server. This value is
+# accessible for authenticated users through the IMAP METADATA server
+# entry "/shared/comment". 
+#mail_server_comment = ""
+
+# Indicates a method for contacting the server administrator. According to
+# RFC 5464, this value MUST be a URI (e.g., a mailto: or tel: URL), but that
+# is currently not enforced. Use for example mailto:admin@example.com. This
+# value is accessible for authenticated users through the IMAP METADATA server
+# entry "/shared/admin".
+#mail_server_admin = 
+
+##
+## Mail processes
+##
+
+# Don't use mmap() at all. This is required if you store indexes to shared
+# filesystems (NFS or clustered filesystem).
+#mmap_disable = no
+
+# Rely on O_EXCL to work when creating dotlock files. NFS supports O_EXCL
+# since version 3, so this should be safe to use nowadays by default.
+#dotlock_use_excl = yes
+
+# When to use fsync() or fdatasync() calls:
+#   optimized (default): Whenever necessary to avoid losing important data
+#   always: Useful with e.g. NFS when write()s are delayed
+#   never: Never use it (best performance, but crashes can lose data)
+#mail_fsync = optimized
+
+# Locking method for index files. Alternatives are fcntl, flock and dotlock.
+# Dotlocking uses some tricks which may create more disk I/O than other locking
+# methods. NFS users: flock doesn't work, remember to change mmap_disable.
+#lock_method = fcntl
+
+# Directory where mails can be temporarily stored. Usually it's used only for
+# mails larger than >= 128 kB. It's used by various parts of Dovecot, for
+# example LDA/LMTP while delivering large mails or zlib plugin for keeping
+# uncompressed mails.
+#mail_temp_dir = /tmp
+
+# Valid UID range for users, defaults to 500 and above. This is mostly
+# to make sure that users can't log in as daemons or other system users.
+# Note that denying root logins is hardcoded to dovecot binary and can't
+# be done even if first_valid_uid is set to 0.
+#first_valid_uid = 500
+#last_valid_uid = 0
+
+# Valid GID range for users, defaults to non-root/wheel. Users having
+# non-valid GID as primary group ID aren't allowed to log in. If user
+# belongs to supplementary groups with non-valid GIDs, those groups are
+# not set.
+#first_valid_gid = 1
+#last_valid_gid = 0
+
+# Maximum allowed length for mail keyword name. It's only forced when trying
+# to create new keywords.
+#mail_max_keyword_length = 50
+
+# ':' separated list of directories under which chrooting is allowed for mail
+# processes (ie. /var/mail will allow chrooting to /var/mail/foo/bar too).
+# This setting doesn't affect login_chroot, mail_chroot or auth chroot
+# settings. If this setting is empty, "/./" in home dirs are ignored.
+# WARNING: Never add directories here which local users can modify, that
+# may lead to root exploit. Usually this should be done only if you don't
+# allow shell access for users. <https://doc.dovecot.org/latest/core/config/chrooting.html>
+#valid_chroot_dirs = 
+
+# Default chroot directory for mail processes. This can be overridden for
+# specific users in user database by giving /./ in user's home directory
+# (eg. /home/./user chroots into /home). Note that usually there is no real
+# need to do chrooting, Dovecot doesn't allow users to access files outside
+# their mail directory anyway. If your home directories are prefixed with
+# the chroot directory, append "/." to mail_chroot. <doc/wiki/Chrooting.txt>
+#mail_chroot = 
+
+# UNIX socket path to master authentication server to find users.
+# This is used by imap (for shared users) and lda.
+#auth_socket_path = /var/run/dovecot/auth-userdb
+
+# Directory where to look up mail plugins.
+#mail_plugin_dir = /usr/lib/dovecot
+
+# Space separated list of plugins to load for all services. Plugins specific to
+# IMAP, LDA, etc. are added to this list in their own .conf files.
+#mail_plugins =
+#
+# To add plugins, use
+#mail_plugins {
+#  plugin = yes
+#}
+
+##
+## Mailbox handling optimizations
+##
+
+# Mailbox list indexes can be used to optimize IMAP STATUS commands. They are
+# also required for IMAP NOTIFY extension to be enabled.
+#mailbox_list_index = yes
+
+# Trust mailbox list index to be up-to-date. This reduces disk I/O at the cost
+# of potentially returning out-of-date results after e.g. server crashes.
+# The results will be automatically fixed once the folders are opened.
+#mailbox_list_index_very_dirty_syncs = yes
+
+# Should INBOX be kept up-to-date in the mailbox list index? By default it's
+# not, because most of the mailbox accesses will open INBOX anyway.
+#mailbox_list_index_include_inbox = no
+
+# The minimum number of mails in a mailbox before updates are done to cache
+# file. This allows optimizing Dovecot's behavior to do less disk writes at
+# the cost of more disk reads.
+#mail_cache_min_mail_count = 0
+
+# When IDLE command is running, mailbox is checked once in a while to see if
+# there are any new mails or other changes. This setting defines the minimum
+# time to wait between those checks. Dovecot can also use inotify and
+# kqueue to find out immediately when changes occur.
+#mailbox_idle_check_interval = 30 secs
+
+# Save mails with CR+LF instead of plain LF. This makes sending those mails
+# take less CPU, especially with sendfile() syscall with Linux and FreeBSD.
+# But it also creates a bit more disk I/O which may just make it slower.
+# Also note that if other software reads the mboxes/maildirs, they may handle
+# the extra CRs wrong and cause problems.
+#mail_save_crlf = no
+
+# Max number of mails to keep open and prefetch to memory. This only works with
+# some mailbox formats and/or operating systems.
+#mail_prefetch_count = 0
+
+# How often to scan for stale temporary files and delete them (0 = never).
+# These should exist only after Dovecot dies in the middle of saving mails.
+#mail_temp_scan_interval = 1w
+
+# How many slow mail accesses sorting can perform before it returns failure.
+# With IMAP the reply is: NO [LIMIT] Requested sort would have taken too long.
+# The untagged SORT reply is still returned, but it's likely not correct.
+#mail_sort_max_read_count = 0
+
+protocol !indexer-worker {
+  # If folder vsize calculation requires opening more than this many mails from
+  # disk (i.e. mail sizes aren't in cache already), return failure and finish
+  # the calculation via indexer process. Disabled by default. This setting must
+  # be 0 for indexer-worker processes.
+  #mail_vsize_bg_after_count = 0
+}
+
+##
+## Maildir-specific settings
+##
+
+# By default LIST command returns all entries in maildir beginning with a dot.
+# Enabling this option makes Dovecot return only entries which are directories.
+# This is done by stat()ing each entry, so it causes more disk I/O.
+# (For systems setting struct dirent->d_type, this check is free and it's
+# done always regardless of this setting)
+#maildir_stat_dirs = no
+
+# When copying a message, do it with hard links whenever possible. This makes
+# the performance much better, and it's unlikely to have any side effects.
+#maildir_copy_with_hardlinks = yes
+
+# Assume Dovecot is the only MUA accessing Maildir: Scan cur/ directory only
+# when its mtime changes unexpectedly or when we can't find the mail otherwise.
+#maildir_very_dirty_syncs = no
+
+# If enabled, Dovecot doesn't use the S=<size> in the Maildir filenames for
+# getting the mail's physical size, except when recalculating Maildir++ quota.
+# This can be useful in systems where a lot of the Maildir filenames have a
+# broken size. The performance hit for enabling this is very small.
+#maildir_broken_filename_sizes = no
+
+# Always move mails from new/ directory to cur/, even when the \Recent flags
+# aren't being reset.
+#maildir_empty_new = no
+
+##
+## mbox-specific settings
+##
+
+# Which locking methods to use for locking mbox. There are four available:
+#  dotlock: Create <mailbox>.lock file. This is the oldest and most NFS-safe
+#           solution. If you want to use /var/mail/ like directory, the users
+#           will need write access to that directory.
+#  dotlock_try: Same as dotlock, but if it fails because of permissions or
+#               because there isn't enough disk space, just skip it.
+#  fcntl  : Use this if possible. Works with NFS too if lockd is used.
+#  flock  : May not exist in all systems. Doesn't work with NFS.
+#  lockf  : May not exist in all systems. Doesn't work with NFS.
+#
+# You can use multiple locking methods; if you do the order they're declared
+# in is important to avoid deadlocks if other MTAs/MUAs are using multiple
+# locking methods as well. Some operating systems don't allow using some of
+# them simultaneously.
+#mbox_read_locks = fcntl
+#mbox_write_locks = dotlock fcntl
+
+# Maximum time to wait for lock (all of them) before aborting.
+#mbox_lock_timeout = 5 mins
+
+# If dotlock exists but the mailbox isn't modified in any way, override the
+# lock file after this much time.
+#mbox_dotlock_change_timeout = 2 mins
+
+# When mbox changes unexpectedly we have to fully read it to find out what
+# changed. If the mbox is large this can take a long time. Since the change
+# is usually just a newly appended mail, it'd be faster to simply read the
+# new mails. If this setting is enabled, Dovecot does this but still safely
+# fallbacks to re-reading the whole mbox file whenever something in mbox isn't
+# how it's expected to be. The only real downside to this setting is that if
+# some other MUA changes message flags, Dovecot doesn't notice it immediately.
+# Note that a full sync is done with SELECT, EXAMINE, EXPUNGE and CHECK 
+# commands.
+#mbox_dirty_syncs = yes
+
+# Like mbox_dirty_syncs, but don't do full syncs even with SELECT, EXAMINE,
+# EXPUNGE or CHECK commands. If this is set, mbox_dirty_syncs is ignored.
+#mbox_very_dirty_syncs = no
+
+# Delay writing mbox headers until doing a full write sync (EXPUNGE and CHECK
+# commands and when closing the mailbox). This is especially useful for POP3
+# where clients often delete all mails. The downside is that our changes
+# aren't immediately visible to other MUAs.
+#mbox_lazy_writes = yes
+
+# If mbox size is smaller than this (e.g. 100k), don't write index files.
+# If an index file already exists it's still read, just not updated.
+#mbox_min_index_size = 0
+
+# Mail header selection algorithm to use for MD5 POP3 UIDLs when
+# pop3_uidl_format=%m. For backwards compatibility we use apop3d inspired
+# algorithm, but it fails if the first Received: header isn't unique in all
+# mails. An alternative algorithm is "all" that selects all headers.
+#mbox_md5 = apop3d
+
+##
+## mdbox-specific settings
+##
+
+# Maximum dbox file size until it's rotated.
+#mdbox_rotate_size = 10M
+
+# Maximum dbox file age until it's rotated. Typically in days. Day begins
+# from midnight, so 1d = today, 2d = yesterday, etc. 0 = check disabled.
+#mdbox_rotate_interval = 0
+
+# When creating new mdbox files, immediately preallocate their size to
+# mdbox_rotate_size. This setting currently works only in Linux with some
+# filesystems (ext4, xfs).
+#mdbox_preallocate_space = no
+
+# Settings to control adding $HasAttachment or $HasNoAttachment keywords.
+# By default, all MIME parts with Content-Disposition=attachment, or inlines
+# with filename parameter are consired attachments.
+#   add-flags - Add the keywords when saving new mails or when fetching can
+#      do it efficiently.
+#   content-type=type or !type - Include/exclude content type. Excluding will
+#     never consider the matched MIME part as attachment. Including will only
+#     negate an exclusion (e.g. content-type=!foo/* content-type=foo/bar).
+#   exclude-inlined - Exclude any Content-Disposition=inline MIME part.
+#mail_attachment_detection_options =
diff --git a/conf/conf.d/10-master.conf b/conf/conf.d/10-master.conf
new file mode 100644 (file)
index 0000000..fc5b288
--- /dev/null
@@ -0,0 +1,133 @@
+#default_process_limit = 100
+#default_client_limit = 1000
+
+# Default VSZ (virtual memory size) limit for service processes. This is mainly
+# intended to catch and kill processes that leak memory before they eat up
+# everything.
+#default_vsz_limit = 256M
+
+# Login user is internally used by login processes. This is the most untrusted
+# user in Dovecot system. It shouldn't have access to anything at all.
+#default_login_user = dovenull
+
+# Internal user is used by unprivileged processes. It should be separate from
+# login user, so that login processes can't disturb other processes.
+#default_internal_user = dovecot
+
+service imap-login {
+  inet_listener imap {
+    #port = 143
+  }
+  inet_listener imaps {
+    #port = 993
+    #ssl = yes
+  }
+
+  # Number of connections to handle before starting a new process. Typically
+  # the only useful values are 0 (unlimited) or 1. 1 is more secure, but 0
+  # is faster. <https://doc.dovecot.org/2.4.1/core/config/service.html#service_restart_request_count>
+  #service_restart_request_count = 1
+
+  # Number of processes to always keep waiting for more connections.
+  #process_min_avail = 0
+
+  # If you set service_restart_request_count=0, you probably need to grow this.
+  #vsz_limit = 256M # default
+}
+
+service pop3-login {
+  inet_listener pop3 {
+    #port = 110
+  }
+  inet_listener pop3s {
+    #port = 995
+    #ssl = yes
+  }
+}
+
+service submission-login {
+  inet_listener submission {
+    #port = 587
+  }
+  inet_listener submissions {
+    #port = 465
+  }
+}
+
+service lmtp {
+  unix_listener lmtp {
+    #mode = 0666
+  }
+
+  # Create inet listener only if you can't use the above UNIX socket
+  #inet_listener lmtp {
+    # Avoid making LMTP visible for the entire internet
+    #listen = 127.0.0.1
+    #port = 24
+  #}
+}
+
+service imap {
+  # Most of the memory goes to mmap()ing files. You may need to increase this
+  # limit if you have huge mailboxes.
+  #vsz_limit = 256M # default
+
+  # Max. number of IMAP processes (connections)
+  #process_limit = 1024
+}
+
+service pop3 {
+  # Max. number of POP3 processes (connections)
+  #process_limit = 1024
+}
+
+service submission {
+  # Max. number of SMTP Submission processes (connections)
+  #process_limit = 1024
+}
+
+service auth {
+  # auth_socket_path points to this userdb socket by default. It's typically
+  # used by dovecot-lda, doveadm, possibly imap process, etc. Users that have
+  # full permissions to this socket are able to get a list of all usernames and
+  # get the results of everyone's userdb lookups.
+  #
+  # The default 0666 mode allows anyone to connect to the socket, but the
+  # userdb lookups will succeed only if the userdb returns an "uid" field that
+  # matches the caller process's UID. Also if caller's uid or gid matches the
+  # socket's uid or gid the lookup succeeds. Anything else causes a failure.
+  #
+  # To give the caller full permissions to lookup all users, set the mode to
+  # something else than 0666 and Dovecot lets the kernel enforce the
+  # permissions (e.g. 0777 allows everyone full permissions).
+  unix_listener auth-userdb {
+    #mode = 0666
+    #user = 
+    #group = 
+  }
+
+  # Postfix smtp-auth
+  #unix_listener /var/spool/postfix/private/auth {
+  #  mode = 0666
+  #}
+
+  # Auth process is run as this user.
+  #user = $SET:default_internal_user
+}
+
+service auth-worker {
+  # Auth worker process is run as root by default, so that it can access
+  # /etc/shadow. If this isn't necessary, the user should be changed to
+  # $SET:default_internal_user.
+  #user = root
+}
+
+service dict {
+  # If dict proxy is used, mail processes should have access to its socket.
+  # For example: mode=0660, group=vmail and global mail_access_groups=vmail
+  unix_listener dict {
+    #mode = 0600
+    #user = 
+    #group = 
+  }
+}
diff --git a/conf/conf.d/10-metrics.conf b/conf/conf.d/10-metrics.conf
new file mode 100644 (file)
index 0000000..36d1729
--- /dev/null
@@ -0,0 +1,122 @@
+##
+## Statistics and metrics
+##
+
+# Dovecot supports gathering statistics from events.
+# Currently there are no statistics logged by default, and therefore they must
+# be explicitly added using the metric configuration blocks.
+#
+# Unlike old stats, the new statistics do not require any plugins loaded.
+#
+# See https://doc.dovecot.org/latest/core/config/statistics.html for details
+
+##
+## Example metrics
+##
+
+#metric auth_success {
+#  filter = (event=auth_request_finished AND success=yes)
+#}
+#
+#metric auth_failure {
+#  filter = (event=auth_request_finished AND NOT success=yes)
+## See exporter config later in this file
+## can be used to replace auth_verbose=yes
+#  exporter = log
+#}
+#
+#metric imap_command {
+#  filter = event=imap_command_finished
+#  group_by cmd_name {
+#    method discrete {
+#    }
+#  }
+#  group_by tagged_reply_state {
+#    method discrete {
+#    }
+#  }
+#}
+#
+#metric smtp_command {
+#  filter = event=smtp_server_command_finished and protocol=submission
+#  group_by cmd_name {
+#     method discrete {
+#    }
+#  }
+#  group_by status_code {
+#     method discrete {
+#    }
+#  }
+#  group_by duration {
+#     method exponential {
+#       base = 10
+#       min_magnitude = 1
+#       max_magnitude = 5
+#    }
+#  }
+#}
+#
+#metric lmtp_command {
+#  filter = event=smtp_server_command_finished and protocol=lmtp
+#  group_by cmd_name {
+#     method discrete {
+#    }
+#  }
+#  group_by status_code {
+#     method discrete {
+#    }
+#  }
+#  group_by duration {
+#     method exponential {
+#       base = 10
+#       min_magnitude = 1
+#       max_magnitude = 5
+#    }
+#  }
+#}
+#
+#metric mail_delivery {
+#  filter = event=mail_delivery_finished
+#  group_by duration {
+#     method exponential {
+#       base = 10
+#       min_magnitude = 1
+#       max_magnitude = 5
+#     }
+#  }
+#}
+
+##
+## Prometheus
+##
+
+# To allow access to statistics with Prometheus, enable http listener
+# on stats process. Stats will be available on /metrics path.
+#
+# See https://doc.dovecot.org/latest/core/config/statistics.html#openmetrics for more
+# details.
+
+#service stats {
+#  inet_listener http {
+#    port = 9900
+#  }
+#}
+
+##
+## Event exporting
+##
+
+# You can also export individual events.
+#
+# See https://doc.dovecot.org/configuration_manual/event_export/ for more
+# details.
+
+#event_exporter log {
+#  format = json
+#  time_format = rfc3339
+#}
+#
+#metric imap_commands {
+#  exporter = log
+#  filter = event=imap_command_finished
+#}
diff --git a/conf/conf.d/10-ssl.conf b/conf/conf.d/10-ssl.conf
new file mode 100644 (file)
index 0000000..d48b15c
--- /dev/null
@@ -0,0 +1,56 @@
+##
+## SSL settings
+##
+
+# SSL/TLS support: yes, no, required. <https://doc.dovecot.org/latest/core/config/ssl.html>
+ssl = yes
+
+# PEM encoded X.509 SSL/TLS certificate and private key.  By default, Debian
+# installs a self-signed certificate.  This is useful for testing, but you
+# should obtain a real certificate from a recognized certificate authority.
+#
+# These files are opened before dropping root privileges, so keep the key file
+# unreadable by anyone but root. Included /usr/share/dovecot/mkcert.sh can be
+# used to easily generate self-signed certificate, just make sure to update the
+# domains in dovecot-openssl.cnf
+# 
+# Preferred permissions: root:root 0444
+ssl_server_cert_file = /etc/dovecot/private/dovecot.pem
+# Preferred permissions: root:root 0400
+ssl_server_key_file = /etc/dovecot/private/dovecot.key
+
+# If key file is password protected, give the password here. Alternatively
+# give it when starting dovecot with -p parameter. Since this file is often
+# world-readable, you may want to place this setting instead to a different
+# root owned 0600 file by using ssl_key_password = <path.
+#ssl_server_key_password =
+
+# PEM encoded trusted certificate authority. Set this only if you intend to use
+# ssl_request_client_cert=yes. The file should contain the CA certificate(s)
+# followed by the matching CRL(s). (e.g. ssl_server_ca_file = /etc/ssl/certs/ca.pem)
+#ssl_server_ca_file = 
+
+# Require that CRL check succeeds for client certificates.
+#ssl_server_require_crl = yes
+
+# Request client to send a certificate. If you also want to require it, set
+# auth_ssl_require_client_cert=yes in auth section.
+#ssl_server_request_client_cert = no
+
+# Which field from certificate to use for username. commonName and
+# x500UniqueIdentifier are the usual choices. You'll also need to set
+# auth_ssl_username_from_cert=yes.
+#ssl_server_cert_username_field = commonName
+
+# SSL protocols to use.  Debian systems specify TLSv1.2 by default, which should
+# be reasonbly secure and compatible with existing clients.
+ssl_min_protocol = TLSv1.2
+# Diffie-Hellman parameters are no longer required and should be phased out.
+# They do not work with ECDH(E) and require DH(E) ciphers.
+#ssl_server_dh_file = /etc/dovecot/dh.pem
+
+# SSL ciphers to use
+#ssl_cipher_list = ALL:!kRSA:!SRP:!kDHd:!DSS:!aNULL:!eNULL:!EXPORT:!DES:!3DES:!MD5:!PSK:!RC4:!ADH:!LOW@STRENGTH
+
+# SSL crypto device to use, for valid values run "openssl engine"
+#ssl_crypto_device = /dev/crypto
diff --git a/conf/conf.d/15-lda.conf b/conf/conf.d/15-lda.conf
new file mode 100644 (file)
index 0000000..d3a4a5f
--- /dev/null
@@ -0,0 +1,50 @@
+##
+## LDA specific settings (also used by LMTP)
+##
+
+# Address to use when sending rejection mails.
+# Default is postmaster@%d. %d expands to recipient domain.
+#postmaster_address =
+
+# Hostname to use in various parts of sent mails (e.g. in Message-Id) and
+# in LMTP replies. Default is the system's real hostname@domain.
+#hostname = 
+
+# If user is over quota, return with temporary failure instead of
+# bouncing the mail.
+#quota_full_tempfail = no
+
+# Binary to use for sending mails.
+#sendmail_path = /usr/sbin/sendmail
+
+# If non-empty, send mails via this SMTP host[:port] instead of sendmail.
+#submission_host =
+
+# Subject: header to use for rejection mails. You can use the same variables
+# as for rejection_reason below.
+#rejection_subject = Rejected: %s
+
+# Human readable error message for rejection mails. You can use variables:
+#  %n = CRLF, %r = reason, %s = original subject, %t = recipient
+#rejection_reason = Your message to <%t> was automatically rejected:%n%r
+
+# Delimiter character between local-part and detail in email address.
+#recipient_delimiter = +
+
+# Header where the original recipient address (SMTP's RCPT TO: address) is taken
+# from if not available elsewhere. With dovecot-lda -a parameter overrides this. 
+# A commonly used header for this is X-Original-To.
+#lda_original_recipient_header =
+
+# Should saving a mail to a nonexistent mailbox automatically create it?
+#lda_mailbox_autocreate = no
+
+# Should automatically created mailboxes be also automatically subscribed?
+#lda_mailbox_autosubscribe = no
+
+protocol lda {
+  # Boolean list of plugins to load
+  #mail_plugins {
+  #   sieve = yes
+  #}
+}
diff --git a/conf/conf.d/15-mailboxes.conf b/conf/conf.d/15-mailboxes.conf
new file mode 100644 (file)
index 0000000..71076d4
--- /dev/null
@@ -0,0 +1,86 @@
+##
+## Mailbox definitions
+##
+
+# Each mailbox is specified in a separate mailbox section. The section name
+# specifies the mailbox name. If it has spaces, you can put the name
+# "in quotes". These sections can contain the following mailbox settings:
+#
+# auto:
+#   Indicates whether the mailbox with this name is automatically created
+#   implicitly when it is first accessed. The user can also be automatically
+#   subscribed to the mailbox after creation. The following values are
+#   defined for this setting:
+# 
+#     no        - Never created automatically.
+#     create    - Automatically created, but no automatic subscription.
+#     subscribe - Automatically created and subscribed.
+#  
+# special_use:
+#   A space-separated list of SPECIAL-USE flags (RFC 6154) to use for the
+#   mailbox. There are no validity checks, so you could specify anything
+#   you want in here, but it's not a good idea to use flags other than the
+#   standard ones specified in the RFC:
+#
+#     \All       - This (virtual) mailbox presents all messages in the
+#                  user's message store.
+#     \Archive   - This mailbox is used to archive messages.
+#     \Drafts    - This mailbox is used to hold draft messages.
+#     \Flagged   - This (virtual) mailbox presents all messages in the
+#                  user's message store marked with the IMAP \Flagged flag.
+#     \Important - This (virtual) mailbox presents all messages in the
+#                  user's message store deemed important to user.
+#     \Junk      - This mailbox is where messages deemed to be junk mail
+#                  are held.
+#     \Sent      - This mailbox is used to hold copies of messages that
+#                  have been sent.
+#     \Trash     - This mailbox is used to hold messages that have been
+#                  deleted.
+#
+# comment:
+#   Defines a default comment or note associated with the mailbox. This
+#   value is accessible through the IMAP METADATA mailbox entries
+#   "/shared/comment" and "/private/comment". Users with sufficient
+#   privileges can override the default value for entries with a custom
+#   value.
+
+# NOTE: Assumes "namespace inbox" has been defined in 10-mail.conf.
+namespace inbox {
+  # These mailboxes are widely used and could perhaps be created automatically:
+  mailbox Drafts {
+    special_use = \Drafts
+  }
+  mailbox Junk {
+    special_use = \Junk
+  }
+  mailbox Trash {
+    special_use = \Trash
+  }
+
+  # For \Sent mailboxes there are two widely used names. We'll mark both of
+  # them as \Sent. User typically deletes one of them if duplicates are created.
+  mailbox Sent {
+    special_use = \Sent
+  }
+  mailbox "Sent Messages" {
+    special_use = \Sent
+  }
+
+  # If you have a virtual "All messages" mailbox:
+  #mailbox virtual/All {
+  #  special_use = \All
+  #  comment = All my messages
+  #}
+
+  # If you have a virtual "Flagged" mailbox:
+  #mailbox virtual/Flagged {
+  #  special_use = \Flagged
+  #  comment = All my flagged messages
+  #}
+
+  # If you have a virtual "Important" mailbox:
+  #mailbox virtual/Important {
+  #  special_use = \Important
+  #  comment = All my important messages
+  #}
+}
diff --git a/conf/conf.d/20-imap.conf b/conf/conf.d/20-imap.conf
new file mode 100644 (file)
index 0000000..34610e9
--- /dev/null
@@ -0,0 +1,108 @@
+##
+## IMAP specific settings
+##
+
+# If nothing happens for this long while client is IDLEing, move the connection
+# to imap-hibernate process and close the old imap process. This saves memory,
+# because connections use very little memory in imap-hibernate process. The
+# downside is that recreating the imap process back uses some resources.
+#imap_hibernate_timeout = 0
+
+# Maximum IMAP command line length. Some clients generate very long command
+# lines with huge mailboxes, so you may need to raise this if you get
+# "Too long argument" or "IMAP command line too large" errors often.
+#imap_max_line_length = 64k
+
+# IMAP logout format string:
+#  %{input} - total number of bytes read from client
+#  %{output} - total number of bytes sent to client
+#  %{fetch_hdr_count} - Number of mails with mail header data sent to client
+#  %{fetch_hdr_bytes} - Number of bytes with mail header data sent to client
+#  %{fetch_body_count} - Number of mails with mail body data sent to client
+#  %{fetch_body_bytes} - Number of bytes with mail body data sent to client
+#  %{deleted} - Number of mails where client added \Deleted flag
+#  %{expunged} - Number of mails that client expunged, which does not
+#                include automatically expunged mails
+#  %{autoexpunged} - Number of mails that were automatically expunged after
+#                    client disconnected
+#  %{trashed} - Number of mails that client copied/moved to the
+#               special_use=\Trash mailbox.
+#  %{appended} - Number of mails saved during the session
+#imap_logout_format = in=%i out=%o deleted=%{deleted} expunged=%{expunged} \
+#  trashed=%{trashed} hdr_count=%{fetch_hdr_count} \
+#  hdr_bytes=%{fetch_hdr_bytes} body_count=%{fetch_body_count} \
+#  body_bytes=%{fetch_body_bytes}
+
+# Amend or override the IMAP capability response. To override, set the value
+# with imap_capability = 
+# 
+# To amend, you can use a boolean list to specify which capabilities to turn
+# on and off
+#imap_capability {
+#  SPECIAL-USE = yes
+#  "LITERAL+" = no
+#}
+
+# How long to wait between "OK Still here" notifications when client is
+# IDLEing.
+#imap_idle_notify_interval = 2 mins
+
+# ID field names and values to send to clients. Using * as the value makes
+# Dovecot use the default value. The following fields have default values
+# currently: name, version, os, os-version, support-url, support-email,
+# revision.
+#imap_id_send = 
+
+# Use imap_id_received event to log IMAP id
+
+# Workarounds for various client bugs:
+#   delay-newmail:
+#     Send EXISTS/RECENT new mail notifications only when replying to NOOP
+#     and CHECK commands. Some clients ignore them otherwise, for example OSX
+#     Mail (<v2.1). Outlook Express breaks more badly though, without this it
+#     may show user "Message no longer in server" errors. Note that OE6 still
+#     breaks even with this workaround if synchronization is set to
+#     "Headers Only".
+#   tb-extra-mailbox-sep:
+#     Thunderbird gets somehow confused with LAYOUT=fs (mbox and dbox) and
+#     adds extra '/' suffixes to mailbox names. This option causes Dovecot to
+#     ignore the extra '/' instead of treating it as invalid mailbox name.
+#   tb-lsub-flags:
+#     Show \Noselect flags for LSUB replies with LAYOUT=fs (e.g. mbox).
+#     This makes Thunderbird realize they aren't selectable and show them
+#     greyed out, instead of only later giving "not selectable" popup error.
+#
+# This is a boolean list
+#imap_client_workarounds {
+#  delay-newmail = yes
+#}
+
+# Host allowed in URLAUTH URLs sent by client. "*" allows all.
+#imap_urlauth_host =
+
+# Enable IMAP LITERAL- extension (replaces LITERAL+)
+#imap_literal_minus = no
+
+# What happens when FETCH fails due to some internal error:
+#   disconnect-immediately:
+#     The FETCH is aborted immediately and the IMAP client is disconnected.
+#   disconnect-after:
+#     The FETCH runs for all the requested mails returning as much data as
+#     possible. The client is finally disconnected without a tagged reply.
+#   no-after:
+#     Same as disconnect-after, but tagged NO reply is sent instead of
+#     disconnecting the client. If the client attempts to FETCH the same failed
+#     mail more than once, the client is disconnected. This is to avoid clients
+#     from going into infinite loops trying to FETCH a broken mail.
+#imap_fetch_failure = disconnect-immediately
+
+protocol imap {
+  # Space separated list of plugins to load (default is global mail_plugins).
+  #mail_plugins {
+  #  imap_sieve = yes
+  #}
+
+  # Maximum number of IMAP connections allowed for a user from each IP address.
+  # NOTE: The username is compared case-sensitively.
+  #mail_max_userip_connections = 10
+}
diff --git a/conf/conf.d/20-lmtp.conf b/conf/conf.d/20-lmtp.conf
new file mode 100644 (file)
index 0000000..ed0af63
--- /dev/null
@@ -0,0 +1,51 @@
+##
+#i
+## LMTP specific settings
+##
+
+# Support proxying to other LMTP/SMTP servers by performing passdb lookups.
+#lmtp_proxy = no
+
+# When recipient address includes the detail (e.g. user+detail), try to save
+# the mail to the detail mailbox. See also recipient_delimiter and
+# lda_mailbox_autocreate settings.
+#lmtp_save_to_detail_mailbox = no
+
+# Verify quota before replying to RCPT TO. This adds a small overhead.
+#lmtp_rcpt_check_quota = no
+
+# Add "Received:" header to mails delivered.
+#lmtp_add_received_header = yes
+
+# Which recipient address to use for Delivered-To: header and Received:
+# header. The default is "final", which is the same as the one given to
+# RCPT TO command. "original" uses the address given in RCPT TO's ORCPT
+# parameter, "none" uses nothing. Note that "none" is currently always used
+# when a mail has multiple recipients.
+#lmtp_hdr_delivery_address = final
+
+# Workarounds for various client bugs:
+#   whitespace-before-path:
+#     Allow one or more spaces or tabs between `MAIL FROM:' and path and between
+#     `RCPT TO:' and path.
+#   mailbox-for-path:
+#     Allow using bare Mailbox syntax (i.e., without <...>) instead of full path
+#     syntax.
+#
+#lmtp_client_workarounds {
+#  whitespace-before-path = yes
+#}
+
+protocol lmtp {
+  #mail_plugins {
+  #  sieve = yes
+  #}
+
+  # This strips the domain name before delivery, since the default
+  # userdb in Debian is /etc/passwd, which doesn't include domain
+  # names in the user.  If you're using a different userdb backend
+  # that does include domain names, you may wish to remove this.  See
+  # https://doc.dovecot.org/2.4.1/howto/lmtp/exim.html and
+  # https://doc.dovecot.org/2.4.1/core/summaries/settings.html#auth_username_format
+  auth_username_format = %{user | username | lower}
+}
diff --git a/conf/conf.d/20-managesieve.conf b/conf/conf.d/20-managesieve.conf
new file mode 100644 (file)
index 0000000..bcee3b8
--- /dev/null
@@ -0,0 +1,78 @@
+##
+## ManageSieve specific settings
+##
+
+# Uncomment to enable managesieve protocol:
+protocols {
+  sieve = yes
+}
+
+# Service definitions
+
+service managesieve-login {
+  inet_listener sieve {
+    port = 4190
+  }
+
+  inet_listener sieve_deprecated {
+    port = 2000
+  }
+
+  # Number of connections to handle before starting a new process. Typically
+  # the only useful values are 0 (unlimited) or 1. 1 is more secure, but 0
+  # is faster. <https://doc.dovecot.org/latest/core/admin/login_processes.html>
+  #service_restart_request_count = 1
+
+  # Number of processes to always keep waiting for more connections.
+  #process_min_avail = 0
+
+  # If you set service_restart_request_count=0, you probably need to grow this.
+  #vsz_limit = 64M
+}
+
+service managesieve {
+  # Max. number of ManageSieve processes (connections)
+  #process_limit = 1024
+}
+
+# Service configuration
+
+protocol sieve {
+  # Maximum ManageSieve command line length in bytes. ManageSieve usually does
+  # not involve overly long command lines, so this setting will not normally
+  # need adjustment
+  #managesieve_max_line_length = 65536
+
+  # Maximum number of ManageSieve connections allowed for a user from each IP
+  # address.
+  # NOTE: The username is compared case-sensitively.
+  #mail_max_userip_connections = 10
+
+  # Space separated list of plugins to load (none known to be useful so far).
+  # Do NOT try to load IMAP plugins here.
+  #mail_plugins =
+
+  # MANAGESIEVE logout format string:
+  #managesieve_logout_format = bytes=%{input}/%{output}
+
+  # To fool ManageSieve clients that are focused on CMU's timesieved you can
+  # specify the IMPLEMENTATION capability that Dovecot reports to clients.
+  # For example: 'Cyrus timsieved v2.2.13'
+  #managesieve_implementation_string = 'Cyrus timsieved v2.2.13'
+
+  # Explicitly specify the SIEVE and NOTIFY capability reported by the server
+  # before login. If left unassigned these will be reported dynamically
+  # according to what the Sieve interpreter supports by default (after login
+  # this may differ depending on the user).
+  #managesieve_sieve_capability {
+  #}
+  #managesieve_notify_capability {
+  #}
+
+  # The maximum number of compile errors that are returned to the client upon
+  # script upload or script verification.
+  #managesieve_max_compile_errors = 5
+
+  # Refer to 90-sieve.conf for script quota configuration and configuration of
+  # Sieve execution limits.
+}
diff --git a/conf/conf.d/20-pop3.conf b/conf/conf.d/20-pop3.conf
new file mode 100644 (file)
index 0000000..f034dbc
--- /dev/null
@@ -0,0 +1,101 @@
+##
+## POP3 specific settings
+##
+
+# Don't try to set mails non-recent or seen with POP3 sessions. This is
+# mostly intended to reduce disk I/O. With maildir it doesn't move files
+# from new/ to cur/, with mbox it doesn't write Status-header.
+#pop3_no_flag_updates = no
+
+# Support LAST command which exists in old POP3 specs, but has been removed
+# from new ones. Some clients still wish to use this though. Enabling this
+# makes RSET command clear all \Seen flags from messages.
+#pop3_enable_last = no
+
+# If mail has X-UIDL header, use it as the mail's UIDL.
+#pop3_reuse_xuidl = no
+
+# Allow only one POP3 session to run simultaneously for the same user.
+#pop3_lock_session = no
+
+# POP3 requires message sizes to be listed as if they had CR+LF linefeeds.
+# Many POP3 servers violate this by returning the sizes with LF linefeeds,
+# because it's faster to get. When this setting is enabled, Dovecot still
+# tries to do the right thing first, but if that requires opening the
+# message, it fallbacks to the easier (but incorrect) size.
+#pop3_fast_size_lookups = no
+
+# POP3 UIDL (unique mail identifier) format to use. You can use following
+# variables with variable extension as described in
+# https://doc.dovecot.org/latest/core/settings/variables.html
+#
+#  %{uidvalidity} - Mailbox's IMAP UIDVALIDITY
+#  %{uid} - Mail's IMAP UID
+#  %{md5} - MD5 sum of the mailbox headers in hex (mbox only)
+#  %{filename} - filename (maildir only)
+#  %{guid} - Mail's GUID
+#
+# If you want UIDL compatibility with other POP3 servers, use:
+#  UW's ipop3d         : %{uid | hex(8)}%{uidvalidity | hex(8)}
+#  Courier             : %{filename} or %{uidvalidity}-%{uid} (both might be used simultaneously)
+#  Cyrus (<= 2.1.3)    : %{uid}
+#  Cyrus (>= 2.1.4)    : %{uidvalidity}.%{uid}
+#  Dovecot v0.99.x     : %{uidvalidity}.%{uid}
+#  tpop3d              : %{filename|md5}
+#
+# Note that Outlook 2003 seems to have problems with %{uidvalidity}.%{uid} format which was
+# Dovecot's default, so if you're building a new server it would be a good
+# idea to change this. %{uid | hex(8)}%{uidvalidity | hex(8)} should be pretty fail-safe.
+#
+#pop3_uidl_format = %{uid | hex(8)}%{uidvalidity | hex(8)}
+
+# Permanently save UIDLs sent to POP3 clients, so pop3_uidl_format changes
+# won't change those UIDLs. Currently this works only with Maildir.
+#pop3_save_uidl = no
+
+# What to do about duplicate UIDLs if they exist?
+#   allow: Show duplicates to clients.
+#   rename: Append a temporary -2, -3, etc. counter after the UIDL.
+#pop3_uidl_duplicates = allow
+
+# This option changes POP3 behavior so that it's not possible to actually
+# delete mails via POP3, only hide them from future POP3 sessions. The mails
+# will still be counted towards user's quota until actually deleted via IMAP.
+# Use e.g. "$POP3Deleted" as the value (it will be visible as IMAP keyword).
+# Make sure you can legally archive mails before enabling this setting.
+#pop3_deleted_flag =
+
+# POP3 logout format string:
+# %{input} - Bytes read from the client
+# %{output} - Bytes sent to the client
+# %{top_count} - Number of TOP commands run
+# %{top_bytes} - Bytes sent to the client because of TOP commands
+# %{retr_count} - Number of RETR commands run
+# %{retr_bytes} - Bytes sent to the client because of RETR commands
+# %{deleted_count} - Number of deleted messages
+# %{deleted_bytes} - Number of bytes in deleted messages
+# %{message_count} - Number of messages before deletion
+# %{message_bytes} - Mailbox size, in bytes, before deletion
+# %{uidl_change} - The old and the new UIDL hash (which can be useful for identifying unexpected changes in UIDLs)
+#pop3_logout_format = top=%{top_count}/%{top_bytes}, retr=%{retr_count}/%{retr_bytes}, del=%{deleted_count}/%{deleted_bytes}, size=%{message_bytes}
+
+# Workarounds for various client bugs:
+#   outlook-no-nuls:
+#     Outlook and Outlook Express hang if mails contain NUL characters.
+#     This setting replaces them with 0x80 character.
+#   oe-ns-eoh:
+#     Outlook Express and Netscape Mail breaks if end of headers-line is
+#     missing. This option simply sends it if it's missing.
+#pop3_client_workarounds {
+#  outlook-no-nuls = yes
+#}
+
+protocol pop3 {
+  # Space separated list of plugins to load (default is global mail_plugins).
+  #mail_plugins {
+  #}
+
+  # Maximum number of POP3 connections allowed for a user from each IP address.
+  # NOTE: The username is compared case-sensitively.
+  #mail_max_userip_connections = 10
+}
diff --git a/conf/conf.d/20-submission.conf b/conf/conf.d/20-submission.conf
new file mode 100644 (file)
index 0000000..1064bca
--- /dev/null
@@ -0,0 +1,114 @@
+##
+## Settings specific to SMTP Submission
+##
+
+# SMTP Submission logout format string:
+#  %{input} - total number of bytes read from client
+#  %{output}- total number of bytes sent to client
+#  %{command_count} - Number of commands received from client
+#  %{reply_count} - Number of replies sent to client
+#  %{session} - Session ID of the login session
+#  %{transaction_id} - ID of the current transaction, if any
+#submission_logout_format = in=%{input} out=%{output}
+
+# Host name reported by the SMTP service, for example to the client in the
+# initial greeting and to the relay server in the HELO/EHLO command.
+# Default is the system's real hostname@domain.
+#hostname =
+
+# Maximum size of messages accepted for relay. This announced in the SIZE
+# capability. If not configured, this is either determined from the relay
+# server or left unlimited if no limit is known (relay will reply with error
+# if some unknown limit exists there, which is duly passed to our client).
+#submission_max_mail_size =
+
+# Maximum number of recipients accepted per connection (default: unlimited)
+#submission_max_recipients =
+
+# Workarounds for various client bugs:
+#   whitespace-before-path:
+#     Allow one or more spaces or tabs between `MAIL FROM:' and path and between
+#     `RCPT TO:' and path.
+#   mailbox-for-path:
+#     Allow using bare Mailbox syntax (i.e., without <...>) instead of full path
+#     syntax.
+#
+#submission_client_workarounds {
+#  whitespace-before-path = yes
+#}
+
+# Relay server configuration:
+#
+# The Dovecot SMTP submission service directly proxies the mail transaction
+# to the SMTP relay configured here.
+
+# Host name for the relay server (required)
+#submission_relay_host =
+
+# Port for the relay server
+#submission_relay_port = 25
+
+# Is the relay server trusted? This determines whether we try to send
+# (Postfix-specific) XCLIENT data to the relay server
+#submission_relay_trusted = no
+
+# Authentication data for the relay server if authentication is required
+#submission_relay_user =
+#submission_relay_master_user =
+#submission_relay_password =
+
+# SSL configuration for connection to relay server
+#
+# submission_relay_ssl:
+#   Indicates whether SSL is used for the connection to the relay server. The
+#   following values are defined for this setting:
+#
+#     no        - No SSL is used
+#     smtps     - An SMTPS connection (immediate SSL) is used
+#     starttls  - The STARTTLS command is used to establish SSL layer
+#submission_relay_ssl = no
+
+# submission_relay_ssl_verify:
+#   Configures whether the SSL certificate of the relay server is to be
+#   verified.
+#submission_relay_ssl_verify = yes
+
+# Write protocol logs for relay connection to this directory for debugging
+#submission_relay_rawlog_dir =
+
+# BURL is configured implicitly by IMAP URLAUTH
+
+# Part of the SMTP capabilities that the submission service can offer to the
+# client (as listed in the EHLO reply) depend on those capabilities also being
+# provided by the relay server. These capabilities currently are:
+#
+#   - 8BITMIME
+#   - BINARYMIME
+#   - DSN
+#   - VRFY (always returns 252 without support)
+#
+# By default, the submission service first connects to the relay server to
+# determine the support for such capabilities before sending the initial EHLO
+# reply to the client. If the list of capabilities returned by the relay server
+# is somehow unreliable or it is undesirable to start the connection to the
+# relay server before the first mail transaction is started, the backend
+# capabilities can be configured explicitly using the
+# submission_backend_capabilities setting. This is a space-separated list of
+# SMTP capability names. This setting is only relevant for capabilities that
+# depend on support from the relay server: including (or omitting) capabilities
+# that are not listed above has no effect. When this setting is explicitly set
+# to the empty string, none of the capabilities is enabled. To achieve the
+# default behavior, this setting must be left unconfigured.
+#submission_backend_capabilities =
+
+protocol submission {
+  # Space-separated list of plugins to load (default is global mail_plugins).
+  #mail_plugins {
+  #}
+
+  # Maximum number of SMTP submission connections allowed for a user from
+  # each IP address.
+  # NOTE: The username is compared case-sensitively.
+  #mail_max_userip_connections = 10
+}
+
diff --git a/conf/conf.d/30-dict-server.conf b/conf/conf.d/30-dict-server.conf
new file mode 100644 (file)
index 0000000..5ff34b0
--- /dev/null
@@ -0,0 +1,34 @@
+##
+## Dictionary server settings
+##
+
+# Dictionary can be used to store key=value lists. This is used by several
+# plugins. The dictionary can be accessed either directly or though a
+# dictionary server. The following dict block maps dictionary names to URIs
+# when the server is used. These can then be referenced using URIs in format
+# "proxy::<name>".
+
+dict_server {
+#  dict quota {
+#    driver = sql
+#    sql_driver = mysql    
+#    hostname = localhost
+#
+#    dict_map priv/quota/storage {
+#      sql_table = quota
+#      username_field = username
+#      value_field bytes {
+#        type = uint
+#      }
+#    }
+#
+#    dict_map priv/quota/messages {
+#      sql_table = quota
+#      username_field = username
+#      value_field messages {
+#        type = uint
+#      }
+#    }
+#  }
+}
+
diff --git a/conf/conf.d/90-acl.conf b/conf/conf.d/90-acl.conf
new file mode 100644 (file)
index 0000000..b3fcfb4
--- /dev/null
@@ -0,0 +1,30 @@
+##
+## Mailbox access control lists.
+##
+
+# vfile backend reads ACLs from "dovecot-acl" file from mail directory.
+# You can also optionally give a global ACL directory path where ACLs are
+# applied to all users' mailboxes. The global ACL directory contains
+# one file for each mailbox, eg. INBOX or sub.mailbox. cache_secs parameter
+# specifies how many seconds to wait between stat()ing dovecot-acl file
+# to see if it changed.
+
+#acl_driver = vfile
+## Deprecated ACL global path
+#acl_global_path = /etc/dovecot/global-acls
+#acl_cache_ttl = 5m
+
+## New inline ACLs
+#mailbox INBOX {
+#  acl user=testuser {
+#    rights = lri
+#  }
+#}
+
+# To let users LIST mailboxes shared by other users, Dovecot needs a
+# shared mailbox dictionary. For example:
+#acl_sharing_map {
+#  dict file {
+#    path = /var/lib/dovecot/shared-mailboxes
+#  }
+#}
diff --git a/conf/conf.d/90-fts-flatcurve.conf b/conf/conf.d/90-fts-flatcurve.conf
new file mode 100644 (file)
index 0000000..da7b1de
--- /dev/null
@@ -0,0 +1,29 @@
+mail_plugins {
+  fts = yes
+  fts_flatcurve = yes
+}
+
+fts_autoindex = yes
+
+language_filters = normalizer-icu snowball stopwords
+
+language_tokenizers = generic email-address
+language_tokenizer_generic_algorithm = simple
+
+language en {
+  default = yes
+  filters = lowercase snowball english-possessive stopwords
+}
+
+fts flatcurve {
+  # All of these are optional, and indicate the default values.
+  # They are listed here for documentation purposes; most people should not
+  # need to define/override in their config.
+  # commit_limit = 500
+  # max_term_size = 30
+  # min_term_size = 2
+  # optimize_limit = 10
+  # rotate_count = 5000
+  # rotate_time = 5000
+  substring_search = yes
+}
diff --git a/conf/conf.d/90-fts.conf b/conf/conf.d/90-fts.conf
new file mode 100644 (file)
index 0000000..e095c9b
--- /dev/null
@@ -0,0 +1,26 @@
+##
+## Plugin settings
+##
+
+# All wanted plugins must be listed in mail_plugins setting before any of the
+# settings take effect. See <https://doc.dovecot.org/latest/> for list of plugins and
+# their configuration. Note that %{variable} expansion is done for all values.
+
+## See https://doc.dovecot.org/latest/core/plugins/fts.html for FTS information
+
+fts_autoindex = yes
+fts_autoindex_max_recent_msgs = 999
+fts_search_add_missing = yes
+
+## for attachment decoding
+
+#fts_decoder_driver = tika
+#fts_decoder_tika_url = http://localhost:9998/tika/
+
+## for solr
+#fts solr {
+#   url = http://localhost:8983/solr/dovecot/
+#}
+
+# See 90-fts-flatcurve.conf in the dovecot-flatcurve package for
+# settings specific to that driver
diff --git a/conf/conf.d/90-quota.conf b/conf/conf.d/90-quota.conf
new file mode 100644 (file)
index 0000000..f3eb2a1
--- /dev/null
@@ -0,0 +1,79 @@
+##
+## Quota configuration.
+##
+
+# Note that you also have to enable quota plugin in mail_plugins setting.
+## <https://doc.dovecot.org/latest/core/plugins/quota.html>
+
+##
+## Quota limits
+##
+
+# Quota limits are set using "quota_rule" parameters. To get per-user quota
+# limits, you can set/override them by returning "quota_rule" extra field
+# from userdb. It's also possible to give mailbox-specific limits, for example
+# to give additional 100 MB when saving to Trash:
+
+#mail_plugins {
+#  quota = yes
+#}
+
+#quota "User quota" {
+#   storage_size = 1G
+#}
+#
+#namespace inbox {
+#   mailbox Trash {
+#     quota_storage_extra = 100M
+#   }
+#}
+
+##
+## Quota warnings
+##
+
+# You can execute a given command when user exceeds a specified quota limit.
+# Each quota root has separate limits. Only the command for the first
+# exceeded limit is executed, so put the highest limit first.
+# The commands are executed via script service by connecting to the named
+# UNIX socket (quota-warning below).
+# Note that % needs to be escaped as %%, otherwise "% " expands to empty.
+
+#quota "User quota" {
+#  warning warn-95 {
+#    quota_storage_percentage = 95
+#    execute quota-warning {
+#      args = 95 %{user}
+#    }
+#  }
+#  warning warn-80 {
+#    quota_storage_percentage = 80
+#    execute quota-warning {
+#      args = 80 %{user}
+#    }
+#  }
+#}
+
+# Example quota-warning service. The unix listener's permissions should be
+# set in a way that mail processes can connect to it. Below example assumes
+# that mail processes run as vmail user. If you use mode=0666, all system users
+# can generate quota warnings to anyone.
+#service quota-warning {
+#  executable = script /usr/local/bin/quota-warning.sh
+#  user = dovecot
+#  unix_listener quota-warning {
+#    user = vmail
+#  }
+#}
+
+##
+## Quota backends
+##
+
+# Multiple backends are supported:
+#   count: Default and recommended, quota driver tracks the quota internally within Dovecot's index files.
+#   maildir: Maildir++ quota
+#   fs: Read-only support for filesystem quota
+#quota "User quota" {
+#  driver = count
+#}
diff --git a/conf/conf.d/90-sieve-extprograms.conf b/conf/conf.d/90-sieve-extprograms.conf
new file mode 100644 (file)
index 0000000..4998fc9
--- /dev/null
@@ -0,0 +1,41 @@
+# Sieve Extprograms plugin configuration
+
+# Don't forget to add the sieve_extprograms plugin to the sieve_plugins setting.
+# Also enable the extensions you need (one or more of vnd.dovecot.pipe,
+# vnd.dovecot.filter and vnd.dovecot.execute) by adding these  to the
+# sieve_extensions or sieve_global_extensions settings. Restricting these
+# extensions to a global context using sieve_global_extensions is recommended.
+
+# The directory where the program sockets are located for the
+# vnd.dovecot.pipe, vnd.dovecot.filter and vnd.dovecot.execute extension
+# respectively. The name of each unix socket contained in that directory
+# directly maps to a program-name referenced from the Sieve script.
+#sieve_pipe_socket_dir = sieve-pipe
+#sieve_filter_socket_dir = sieve-filter
+#sieve_execute_socket_dir = sieve-execute
+
+# The directory where the scripts are located for direct execution by the
+# vnd.dovecot.pipe, vnd.dovecot.filter and vnd.dovecot.execute extension
+# respectively. The name of each script contained in that directory
+# directly maps to a program-name referenced from the Sieve script.
+#sieve_pipe_bin_dir = /usr/lib/dovecot/sieve-pipe
+#sieve_filter_bin_dir = /usr/lib/dovecot/sieve-filter
+#sieve_execute_bin_dir = /usr/lib/dovecot/sieve-execute
+
+# An example program service called 'do-something' to pipe messages to
+#service do-something {
+  # Define the executed script as parameter to the sieve service
+  #executable = script /usr/lib/dovecot/sieve-pipe/do-something.sh
+
+  # Use some unprivileged user for executing the program
+  #user = dovenull
+
+  # The unix socket located in the sieve_pipe_socket_dir (as defined in the 
+  # plugin {} section above)
+  #unix_listener sieve-pipe/do-something {
+    # LDA/LMTP must have access
+  #  user = vmail  
+  #  mode = 0600
+  #}
+#}
+
diff --git a/conf/conf.d/90-sieve.conf b/conf/conf.d/90-sieve.conf
new file mode 100644 (file)
index 0000000..3ab66d3
--- /dev/null
@@ -0,0 +1,113 @@
+##
+## Settings for the Sieve interpreter
+##
+
+# Do not forget to enable the Sieve plugin in 15-lda.conf and 20-lmtp.conf
+# by adding it to the respective mail_plugins { sieve = yes } settings.
+
+# See https://doc.dovecot.org/latest/core/plugins/sieve.html
+
+# Personal sieve script location
+#sieve_script personal {
+#  driver = file
+#  path = ~/sieve
+#  active_path = ~/.dovecot.sieve
+#}
+
+# Default sieve script location
+#sieve_script default {
+#  type = default
+#  name = default
+#  driver = file
+#  path = /etc/dovecot/sieve/default/
+#}
+
+
+# Which Sieve language extensions are available to users. By default, all
+# supported extensions are available, except for deprecated extensions or
+# those that are still under development. Some system administrators may want
+# to disable certain Sieve extensions or enable those that are not available
+# by default. This setting can use 'yes' and 'no' to specify differences relative
+# to the default. For example `imapflags = yes' will enable the
+# deprecated imapflags extension in addition to all extensions were already
+# enabled by default.
+#sieve_extensions {
+#  mboxmetadata = yes
+#  vnd.dovecot.debug = yes
+#}
+
+# Which Sieve language extensions are ONLY available in global scripts. This
+# can be used to restrict the use of certain Sieve extensions to administrator
+# control, for instance when these extensions can cause security concerns.
+# This setting has higher precedence than the `sieve_extensions' setting
+# (above), meaning that the extensions enabled with this setting are never
+# available to the user's personal script no matter what is specified for the
+# `sieve_extensions' setting. The syntax of this setting is similar to the
+# `sieve_extensions' setting, with the difference that extensions are
+# enabled or disabled for exclusive use in global scripts. Currently, no
+# extensions are marked as such by default.
+#sieve_global_extensions =
+
+# The Pigeonhole Sieve interpreter can have plugins of its own. Using this
+# setting, the used plugins can be specified. Check the Dovecot documentation
+# https://doc.dovecot.org/latest/core/plugins/sieve.html
+
+#sieve_plugins = sieve_imapsieve sieve_extprograms
+#sieve_pipe_bin_dir    = /usr/share/dovecot-pigeonhole/sieve
+#sieve_execute_bin_dir = /usr/share/dovecot-pigeonhole/sieve
+#sieve_global_extensions {
+#  vnd.dovecot.pipe = yes
+#  vnd.dovecot.execute = yes
+#}
+#imapsieve_url = 
+
+# The separator that is expected between the :user and :detail
+# address parts introduced by the subaddress extension. This may
+# also be a sequence of characters (e.g. '--'). The current
+# implementation looks for the separator from the left of the
+# localpart and uses the first one encountered. The :user part is
+# left of the separator and the :detail part is right. This setting
+# is also used by Dovecot's LMTP service.
+#recipient_delimiter = +-_
+
+# The maximum size of a Sieve script. The compiler will refuse to compile any
+# script larger than this limit. If set to 0, no limit on the script size is
+# enforced.
+#sieve_max_script_size = 1M
+
+# The maximum number of actions that can be performed during a single script
+# execution. If set to 0, no limit on the total number of actions is enforced.
+#sieve_max_actions = 32
+
+# The maximum number of redirect actions that can be performed during a single
+# script execution. If set to 0, no redirect actions are allowed.
+#sieve_max_redirects = 4
+
+# The maximum number of personal Sieve scripts a single user can have. If set
+# to 0, no limit on the number of scripts is enforced.
+# (Currently only relevant for ManageSieve)
+#sieve_quota_script_count = 0
+
+# The maximum amount of disk storage a single user's scripts may occupy. If
+# set to 0, no limit on the used amount of disk storage is enforced.
+# (Currently only relevant for ManageSieve)
+#sieve_quota_storage_size = 0
+
+
+#mailbox Spam {
+## From elsewhere to Spam folder
+#  sieve_script report-spam {
+#    type = before
+#    cause = copy
+#    path = /etc/dovecot/report-spam.sieve
+#  }
+#}
+
+## From Spam folder to elsewhere
+#imapsieve_from Spam {
+#  sieve_script report-ham {
+#    type = before
+#    cause = copy
+#    path = /etc/dovecot/report-ham.sieve
+#  }
+#}
diff --git a/conf/conf.d/99-local.conf b/conf/conf.d/99-local.conf
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/conf/conf.d/auth-deny.conf.ext b/conf/conf.d/auth-deny.conf.ext
new file mode 100644 (file)
index 0000000..35b5c19
--- /dev/null
@@ -0,0 +1,15 @@
+# Deny access for users. Included from auth.conf.
+
+# Users can be (temporarily) disabled by adding a passdb with deny=yes.
+# If the user is found from that database, authentication will fail.
+# The deny passdb should always be specified before others, so it gets
+# checked first.
+
+# Example deny passdb using passwd-file. You can use any passdb though.
+#passdb deny {
+#  driver = passwd-file
+#  deny = yes
+#
+## File contains a list of usernames, one per line
+#  passwd_file_path = /etc/dovecot/deny-users
+#}
diff --git a/conf/conf.d/auth-ldap.conf.ext b/conf/conf.d/auth-ldap.conf.ext
new file mode 100644 (file)
index 0000000..4e5982b
--- /dev/null
@@ -0,0 +1,48 @@
+# Authentication for LDAP users. Included from auth.conf.
+#
+# <https://doc.dovecot.org/latest/core/config/auth/databases/ldap.html>
+
+## See <https://doc.dovecot.org/latest/core/config/dict.html#ldap>
+ 
+#ldap_uris = ldap://localhost
+#ldap_auth_dn = cn=admin 
+#ldap_auth_dn_password = supersecret
+
+#passdb ldap {
+#  ldap_filter = (&(objectClass=posixAccount)(uid=%{user}))
+#  ldap_bind = no
+
+#  fields {
+    # user=%{ldap:uid}
+    # password=%{ldap:userPassword}
+    # userdb_home=%{ldap:homeDirectory}
+    # userdb_uid=%{ldap:uidNumber}
+    # userdb_gid=%{ldap:gidNumber}
+#  }
+#}
+
+# "prefetch" user database means that the passdb already provided the
+# needed information and there's no need to do a separate userdb lookup.
+# <https://doc.dovecot.org/latest/core/config/auth/databases/prefetch.html>
+#userdb prefetch {
+#}
+
+#userdb ldap {
+#  ldap_filter = (&(objectClass=posixAccount)(uid=%{user}))
+
+# Default fields can be used to specify defaults that LDAP may override
+#  fields {
+#    home=/home/virtual/%{user}
+#  }
+#}
+
+# If you don't have any user-specific settings, you can avoid the userdb LDAP
+# lookup by using userdb static instead of userdb ldap, for example:
+# <https://doc.dovecot.org/latest/core/config/auth/databases/static.html>
+#userdb static {
+  #fields {
+  #  uid = vmail
+  #  gid = vmail
+  #  home = /var/vmail/%{user}
+  #}
+#}
diff --git a/conf/conf.d/auth-master.conf.ext b/conf/conf.d/auth-master.conf.ext
new file mode 100644 (file)
index 0000000..17839b2
--- /dev/null
@@ -0,0 +1,12 @@
+# Authentication for master users. Included from auth.conf.
+
+# By adding master=yes setting inside a passdb you make the passdb a list
+# of "master users", who can log in as anyone else.
+# <https://doc.dovecot.org/latest/core/config/auth/master_users.html>
+
+# Example master user passdb using passwd-file. You can use any passdb though.
+#passdb master-passwd-file {
+#  driver = passwd-file
+#  master = yes
+#  passwd_file_path = /etc/dovecot/master-users
+#}
diff --git a/conf/conf.d/auth-oauth2.conf.ext b/conf/conf.d/auth-oauth2.conf.ext
new file mode 100644 (file)
index 0000000..ce90f91
--- /dev/null
@@ -0,0 +1,23 @@
+#auth_mechanisms {
+#  xoauth2 = yes
+#  oauthbearer = yes
+#}
+#oauth2 {
+#  tokeninfo_url = https://www.googleapis.com/oauth2/v3/tokeninfo?access_token=
+#  introspection_url = https://www.googleapis.com/oauth2/v2/userinfo
+#  #force_introspection = yes
+#  username_attribute = email
+#}
+
+# with local validation
+#oauth2 {
+#  introspection_mode = local
+#  username_attribute = email
+#  oauth2_local_validation {
+#    dict fs {
+#      fs posix {
+#        prefix = /etc/dovecot/oauth2-keys/
+#      }
+#    }
+#  }
+#}
diff --git a/conf/conf.d/auth-passwdfile.conf.ext b/conf/conf.d/auth-passwdfile.conf.ext
new file mode 100644 (file)
index 0000000..033f253
--- /dev/null
@@ -0,0 +1,20 @@
+# Authentication for passwd-file users. Included from auth.conf.
+#
+# passwd-like file with specified location.
+# <https://doc.dovecot.org/latest/core/config/auth/databases/passwd_file.html>
+
+#passdb passwd-file {
+#  default_password_scheme = crypt
+#  auth_username_format = %{user}
+#  passwd_file_path = /etc/dovecot/users
+#}
+
+#userdb passwd-file {
+#  auth_username_format = %{user}
+#  passwd_file_path = /etc/dovecot/users
+
+#  fields {
+#    quota_rule:default=*:storage=1G
+#    home=/home/virtual/%{user}
+#  }
+#}
diff --git a/conf/conf.d/auth-sql.conf.ext b/conf/conf.d/auth-sql.conf.ext
new file mode 100644 (file)
index 0000000..4b8f504
--- /dev/null
@@ -0,0 +1,180 @@
+# Authentication for SQL users. Included from auth.conf.
+#
+# <https://doc.dovecot.org/latest/core/config/auth/databases/sql.html>
+
+# For the sql passdb module, you'll need a database with a table that
+# contains fields for at least the username and password. If you want to
+# use the user@domain syntax, you might want to have a separate domain
+# field as well.
+#
+# If your users all have the same uig/gid, and have predictable home
+# directories, you can use the static userdb module to generate the home
+# dir based on the username and domain. In this case, you won't need fields
+# for home, uid, or gid in the database.
+#
+# If you prefer to use the sql userdb module, you'll want to add fields
+# for home, uid, and gid. Here is an example table:
+#
+# CREATE TABLE users (
+#     username VARCHAR(128) NOT NULL,
+#     domain VARCHAR(128) NOT NULL,
+#     password VARCHAR(64) NOT NULL,
+#     home VARCHAR(255) NOT NULL,
+#     uid INTEGER NOT NULL,
+#     gid INTEGER NOT NULL,
+#     active CHAR(1) DEFAULT 'Y' NOT NULL
+# );
+
+# Database driver: mysql, pgsql, sqlite
+#sql_driver = mysql
+
+# Database connection string. This is driver-specific setting.
+#
+# HA / round-robin load-balancing is supported by giving multiple host
+# settings, like: host=sql1.host.org host=sql2.host.org
+#
+# pgsql:
+#   For available options, see the PostgreSQL documention for the
+#   PQconnectdb function of libpq.
+#   Use maxconns=n (default 5) to change how many connections Dovecot can
+#   create to pgsql.
+#
+# mysql:
+#   Basic options emulate PostgreSQL option names:
+#     host, port, user, password, dbname
+#
+#   But also adds some new settings:
+#     client_flags        - See MySQL manual
+#     ssl_ca, ssl_ca_path - Set either one or both to enable SSL
+#     ssl_cert, ssl_key   - For sending client-side certificates to server
+#     ssl_cipher          - Set minimum allowed cipher security (default: HIGH)
+#     option_file         - Read options from the given file instead of
+#                           the default my.cnf location
+#     option_group        - Read options from the given group (default: client)
+# 
+#   You can connect to UNIX sockets by using host: host=/var/run/mysql.sock
+#   Note that currently you can't use spaces in parameters.
+#
+# sqlite:
+#   The path to the database file.
+#
+# Examples:
+# mysql 192.168.1.1 {
+#   dbname = users
+# }
+# mysql sql.example.com {
+#   ssl = yes
+#   user = virtual
+#   password = blarg
+#   dbname = virtual
+# }
+# sqlite /etc/dovecot/authdb.sqlite {
+# }
+#
+#mysql /var/run/mysqld/mysqld.sock {
+#  user = dovecot
+#  password = dvmail
+#  dbname = dovecot
+#}
+#mysql localhost {
+# ...
+#}
+
+#passdb sql {
+#  default_password_scheme = SHA256
+
+# passdb query to retrieve the password. It can return fields:
+#   password - The user's password. This field must be returned.
+#   user - user@domain from the database. Needed with case-insensitive lookups.
+#   username and domain - An alternative way to represent the "user" field.
+#
+# The "user" field is often necessary with case-insensitive lookups to avoid
+# e.g. "name" and "nAme" logins creating two different mail directories. If
+# your user and domain names are in separate fields, you can return "username"
+# and "domain" fields instead of "user".
+#
+# The query can also return other fields which have a special meaning, see
+# https://doc.dovecot.org/latest/core/config/auth/passdb.html#extra-fields
+#
+# Commonly used available substitutions (see https://doc.dovecot.org/latest/core/settings/variables.html
+# for full list):
+#   %{user} = entire user@domain
+#   %{user|username} = user part of user@domain
+#   %{user|domain} = domain part of user@domain
+# 
+# Note that these can be used only as input to SQL query. If the query outputs
+# any of these substitutions, they're not touched. Otherwise it would be
+# difficult to have eg. usernames containing '%' characters.
+#
+# Example:
+#   query = SELECT userid AS user, pw AS password \
+#     FROM users WHERE userid = '%u' AND active = 'Y'
+#
+#  query = \
+#    SELECT userid as username, domain, password \
+#    FROM users WHERE userid = '%{user|username}' AND domain = '%{user|domain}'
+#}
+
+#userdb sql {
+# userdb query to retrieve the user information. It can return fields:
+#   uid - System UID (overrides mail_uid setting)
+#   gid - System GID (overrides mail_gid setting)
+#   home - Home directory
+#   mail_driver - Mail driver
+#   mail_path - Mail storage path
+#
+# None of these are strictly required. If you use a single UID and GID, and
+# home or mail directory fits to a template string, you could use userdb static
+# instead. For a list of all fields that can be returned, see
+# Examples:
+#   query = SELECT home, uid, gid FROM users WHERE userid = '%{user}'
+#   query = SELECT dir AS home, user AS uid, group AS gid FROM users where userid = '%{user}'
+#   query = SELECT home, 501 AS uid, 501 AS gid FROM users WHERE userid = '%{user}'
+#
+#  query = \
+#    SELECT home, uid, gid \
+#    FROM users WHERE userid = '%{user|username}' AND domain = '%{user|domain}'
+
+# Query to get a list of all usernames.
+#  iterate_query = SELECT username AS user,domain FROM users
+
+#  userdb_ldap {
+#    iterate_fields {
+#      home = /var/vmail/%{home}
+#    }
+#  }
+#}
+
+#passdb static {
+#  fields {
+#    user=%{user|username|lower}
+#    noauthenticate=yes
+#  }
+## you can remove next line if you want to always normalize your usernames
+#  skip = authenticated
+#}
+
+# "prefetch" user database means that the passdb already provided the
+# needed information and there's no need to do a separate userdb lookup.
+# <https://doc.dovecot.org/latest/core/config/auth/databases/prefetch.html>
+#userdb prefetch {
+#}
+
+#userdb static {
+#  fields {
+#    user=%{user|lower}
+#  }
+# you can remove next line if you want to always normalize your usernames
+#  skip = found
+#}
+
+# If you don't have any user-specific settings, you can avoid the user_query
+# by using userdb static instead of userdb sql, for example:
+# <https://doc.dovecot.org/latest/core/config/auth/databases/static.html>
+#userdb static {
+  #fields {
+  #  uid = vmail
+  #  gid = vmail
+  #  home = /var/vmail/%{user}
+  #}
+#}
diff --git a/conf/conf.d/auth-static.conf.ext b/conf/conf.d/auth-static.conf.ext
new file mode 100644 (file)
index 0000000..d8ddb54
--- /dev/null
@@ -0,0 +1,35 @@
+# Static passdb. Included from auth.conf.
+
+# This can be used for situations where Dovecot doesn't need to verify the
+# username or the password, or if there is a single password for all users:
+#
+#  - proxy frontend, where the backend verifies the password
+#  - proxy backend, where the frontend already verified the password
+#  - authentication with SSL certificates
+#  - simple testing
+# 
+# Each passdb must have unique name, but if you onlu use it once per driver
+# you can use driver name as passdb/userdb section name.
+
+#passdb static {
+#  fields {
+#    proxy = y
+#    host = %{user|md5|substr(1)}.example.com
+#    nopassword = y
+#  }
+#}
+
+#passdb static2 {
+#  driver = static
+#  fields {
+#    password = test
+#  }
+#}
+
+#userdb static {
+#  fields {
+#    uid = vmail
+#    gid = vmail
+#    home = /home/%{user}
+#  }
+#}
diff --git a/conf/conf.d/auth-system.conf.ext b/conf/conf.d/auth-system.conf.ext
new file mode 100644 (file)
index 0000000..237db61
--- /dev/null
@@ -0,0 +1,72 @@
+# Authentication for system users. Included from auth.conf.
+#
+# <https://doc.dovecot.org/latest/core/config/auth/passdb.html>
+# <https://doc.dovecot.org/latest/core/config/auth/userdb.html>
+
+# Driver is only needed if the section name is not same as driver's name.
+
+# PAM authentication. Preferred nowadays by most systems.
+# PAM is typically used with either userdb passwd or userdb static.
+# REMEMBER: You'll need /etc/pam.d/dovecot file created for PAM
+# authentication to actually work. <https://doc.dovecot.org/latest/core/config/auth/databases/pam.html>
+passdb pam {
+#  driver = pam
+#  session = yes
+#  setcred = yes
+#  failure_show_msg = yes
+#  max_requests = 20
+#  service_name = dovecot
+#  skip = authenticated 
+#  fields {
+#  }
+}
+
+userdb passwd {
+}
+
+# System users (NSS, /etc/passwd, or similar).
+# In many systems nowadays this uses Name Service Switch, which is
+# configured in /etc/nsswitch.conf. <https://doc.dovecot.org/latest/core/config/auth/databases/passwd.html>
+#passdb passwb {
+#}
+
+# PAM-like authentication for OpenBSD.
+# <https://doc.dovecot.org/latest/core/config/auth/databases/bsd.html>
+#passdb bsdauth {
+#}
+
+##
+## User databases
+##
+
+# System users (NSS, /etc/passwd, or similiar). In many systems nowadays this
+# uses Name Service Switch, which is configured in /etc/nsswitch.conf.
+#userdb passwd-file {
+  #driver = passwd-file
+  #auth_username_format=%{user|lower}
+  #passwd_file_path = /etc/passwd
+  #fields {
+  #  user= %{user|lower}
+  #  name = %{user|lower}
+  #  home = /var/vmail/%{user}
+  #}
+  #skip = found
+#}
+
+# Static settings generated from template <https://doc.dovecot.org/latest/core/config/auth/databases/static.html>
+#userdb static {
+  #driver = static
+  # Can return anything a userdb could normally return. For example:
+  # fields {
+  #   uid = 500
+  #   gid = 500
+  #   home = /var/mail/%{user}
+  # }
+  # LDA and LMTP needs to look up users only from the userdb. This of course
+  # doesn't work with static userdb because there is no list of users.
+  # Normally static userdb handles this by doing a passdb lookup. This works
+  # with most passdbs, with PAM being the most notable exception. If you do
+  # the user verification another way, you can add allow_all_users=yes
+  # in which case the passdb lookup is skipped.
+  #allow_all_users = yes
+#}
diff --git a/conf/dovecot.conf b/conf/dovecot.conf
new file mode 100644 (file)
index 0000000..ee7eb33
--- /dev/null
@@ -0,0 +1,87 @@
+## Dovecot configuration file
+
+# If you're in a hurry, see https://doc.dovecot.org/latest/core/config/guides/quick.html
+
+# "doveconf -n" command gives a clean output of the changed settings. Use it
+# instead of copy&pasting files when posting to the Dovecot mailing list.
+
+# '#' character and everything after it is treated as comments. Extra spaces
+# and tabs are ignored. If you want to use either of these explicitly, put the
+# value inside quotes, eg.: key = "# char and trailing whitespace  "
+
+# Default values are shown for each setting, it's not required to uncomment
+# those. These are exceptions to this though: No sections (e.g. namespace {})
+# or plugin settings are added by default, they're listed only as examples.
+# Paths are also just examples with the real defaults being based on configure
+# options. The paths listed here are for configure --prefix=/usr/local
+# --sysconfdir=/usr/local/etc --localstatedir=/var
+
+dovecot_config_version = 2.4.0
+dovecot_storage_version = 2.4.0
+
+# Protocols we want to be serving.
+#protocols = imap pop3 lmtp
+!include_try /usr/share/dovecot/protocols.d/*.protocol
+
+# A comma separated list of IPs or hosts where to listen in for connections. 
+# "*" listens in all IPv4 interfaces, "::" listens in all IPv6 interfaces.
+# If you want to specify non-default ports or anything more complex,
+# edit conf.d/master.conf.
+#listen = *, ::
+
+# Base directory where to store runtime data.
+#base_dir = /var/run/dovecot/
+
+# Name of this instance. In multi-instance setup doveadm and other commands
+# can use -i <instance_name> to select which instance is used (an alternative
+# to -c <config_path>). The instance name is also added to Dovecot processes
+# in ps output.
+#instance_name = dovecot
+
+# Greeting message for clients.
+#login_greeting = Dovecot ready.
+
+# Space separated list of trusted network ranges. Connections from these
+# IPs are allowed to override their IP addresses and ports (for logging and
+# for authentication checks). disable_plaintext_auth is also ignored for
+# these networks, unless ssl=required.
+# Typically you'd specify your IMAP proxy servers here.
+#login_trusted_networks =
+
+# With proxy_maybe=yes if proxy destination matches any of these IPs, don't do
+# proxying. This isn't necessary normally, but may be useful if the destination
+# IP is e.g. a load balancer's IP.
+#auth_proxy_self =
+
+# Show more verbose process titles (in ps). Currently shows user name and
+# IP address. Useful for seeing who are actually using the IMAP processes
+# (eg. shared mailboxes or if same uid is used for multiple accounts).
+#verbose_proctitle = yes
+
+# Should all processes be killed when Dovecot master process shuts down.
+# Setting this to "no" means that Dovecot can be upgraded without
+# forcing existing client connections to close (although that could also be
+# a problem if the upgrade is e.g. because of a security fix).
+#shutdown_clients = yes
+
+# If non-zero, run mail commands via this many connections to doveadm server,
+# instead of running them directly in the same process.
+#doveadm_worker_count = 0
+# UNIX socket or host:port used for connecting to doveadm server
+#doveadm_socket_path = doveadm-server
+
+# Space separated list of environment variables that are preserved on Dovecot
+# startup and passed down to all of its child processes. You can also give
+# key=value pairs to always set specific settings.
+#import_environment {
+#  TZ=%{env:TZ}
+#}
+
+# Most of the actual configuration gets included below. The filenames are
+# first sorted by their ASCII value and parsed in that order. The 00-prefixes
+# in filenames are intended to make it easier to understand the ordering.
+!include conf.d/*.conf
+
+# A config file can also tried to be included without giving an error if
+# it's not found:
+!include_try local.conf
diff --git a/conf/local.conf b/conf/local.conf
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/control b/control
new file mode 100644 (file)
index 0000000..5ae13e3
--- /dev/null
+++ b/control
@@ -0,0 +1,304 @@
+Source: dovecot
+Section: mail
+Priority: optional
+Maintainer: Dovecot Maintainers <dovecot@packages.debian.org>
+Uploaders: Noah Meyerhans <noahm@debian.org>
+Build-Depends: debhelper-compat (= 13),
+               default-libmysqlclient-dev,
+               krb5-multidev,
+               libapparmor-dev [linux-any],
+               libbz2-dev,
+               libcap-dev [linux-any],
+              libcdb-dev,
+               libdb-dev,
+               libexpat-dev,
+               libexttextcat-dev,
+               libicu-dev,
+               libldap2-dev,
+               liblua5.4-dev,
+               liblz4-dev,
+               liblzma-dev,
+               libpam0g-dev,
+               libpq-dev,
+               libsasl2-dev,
+               libsodium-dev,
+               libsqlite3-dev,
+               libssl-dev,
+               libstemmer-dev,
+               libsystemd-dev [linux-any],
+               libtirpc-dev,
+               libunwind-dev,
+               libwrap0-dev,
+               libxapian-dev,
+               libzstd-dev,
+               lsb-release,
+               lua-json <!nocheck>,
+               pkgconf,
+               zlib1g-dev
+Standards-Version: 4.7.0
+Rules-Requires-Root: no
+Homepage: https://dovecot.org/
+Vcs-Git: https://salsa.debian.org/debian/dovecot.git
+Vcs-Browser: https://salsa.debian.org/debian/dovecot
+
+Package: dovecot-core
+Architecture: any
+Pre-Depends: ${misc:Pre-Depends}
+Depends: adduser,
+         libpam-runtime,
+         openssl,
+         ssl-cert,
+         ucf,
+         ${misc:Depends},
+         ${shlibs:Depends}
+Suggests: dovecot-flatcurve,
+          dovecot-gssapi,
+          dovecot-imapd,
+          dovecot-ldap,
+          dovecot-lmtpd,
+          dovecot-managesieved,
+          dovecot-mysql,
+          dovecot-pgsql,
+          dovecot-pop3d,
+          dovecot-sieve,
+          dovecot-solr,
+          dovecot-sqlite,
+          dovecot-submissiond,
+          ntp
+Provides: dovecot-abi-${dovecot:ABI-Version}, dovecot-common
+Replaces: dovecot-common (<< 1:2.0.14-2~), mailavenger (<< 0.8.1-4)
+Breaks: dovecot-common (<< 1:2.0.14-2~), mailavenger (<< 0.8.1-4)
+Description: secure POP3/IMAP server - core files
+ Dovecot is a mail server whose major goals are security and extreme
+ reliability. It tries very hard to handle all error conditions and verify
+ that all data is valid, making it nearly impossible to crash. It supports
+ mbox/Maildir and its own dbox/mdbox formats, and should also be pretty
+ fast, extensible, and portable.
+ .
+ This package contains the Dovecot main server and its command line utility.
+
+Package: dovecot-dev
+Architecture: any
+Depends: ${misc:Depends}, ${shlibs:Depends}
+Replaces: dovecot-common (<< 1:2.0.14-2~), dovecot-core (<< 1:2.2.26.0-1~)
+Breaks: dovecot-common (<< 1:2.0.14-2~), dovecot-core (<< 1:2.2.26.0-1~)
+Description: secure POP3/IMAP server - header files
+ Dovecot is a mail server whose major goals are security and extreme
+ reliability. It tries very hard to handle all error conditions and verify
+ that all data is valid, making it nearly impossible to crash. It supports
+ mbox/Maildir and its own dbox/mdbox formats, and should also be pretty
+ fast, extensible, and portable.
+ .
+ This package contains header files needed to compile plugins for the Dovecot
+ mail server.
+
+Package: dovecot-imapd
+Architecture: any
+Depends: dovecot-core (= ${binary:Version}),
+         ucf,
+         ${misc:Depends},
+         ${shlibs:Depends}
+Suggests: ufw
+Provides: imap-server
+Description: secure POP3/IMAP server - IMAP daemon
+ Dovecot is a mail server whose major goals are security and extreme
+ reliability. It tries very hard to handle all error conditions and verify
+ that all data is valid, making it nearly impossible to crash. It supports
+ mbox/Maildir and its own dbox/mdbox formats, and should also be pretty
+ fast, extensible, and portable.
+ .
+ This package contains the Dovecot IMAP server.
+
+Package: dovecot-pop3d
+Architecture: any
+Depends: dovecot-core (= ${binary:Version}),
+         ucf,
+         ${misc:Depends},
+         ${shlibs:Depends}
+Suggests: ufw
+Provides: pop3-server
+Description: secure POP3/IMAP server - POP3 daemon
+ Dovecot is a mail server whose major goals are security and extreme
+ reliability. It tries very hard to handle all error conditions and verify
+ that all data is valid, making it nearly impossible to crash. It supports
+ mbox/Maildir and its own dbox/mdbox formats, and should also be pretty
+ fast, extensible, and portable.
+ .
+ This package contains the Dovecot POP3 server.
+
+Package: dovecot-lmtpd
+Architecture: any
+Depends: dovecot-core (= ${binary:Version}),
+         ucf,
+         ${misc:Depends},
+         ${shlibs:Depends}
+Replaces: dovecot-common (<< 1:2.0.14-2~)
+Breaks: dovecot-common (<< 1:2.0.14-2~)
+Description: secure POP3/IMAP server - LMTP server
+ Dovecot is a mail server whose major goals are security and extreme
+ reliability. It tries very hard to handle all error conditions and verify
+ that all data is valid, making it nearly impossible to crash. It supports
+ mbox/Maildir and its own dbox/mdbox formats, and should also be pretty
+ fast, extensible, and portable.
+ .
+ This package contains the Dovecot LMTP server.
+
+Package: dovecot-managesieved
+Architecture: any
+Depends: dovecot-core (= ${binary:Version}),
+         dovecot-sieve (= ${binary:Version}),
+         ucf,
+         ${misc:Depends},
+         ${shlibs:Depends}
+Replaces: dovecot-common (<< 1:2.0.14-2~)
+Breaks: dovecot-common (<< 1:2.0.14-2~)
+Description: secure POP3/IMAP server - ManageSieve server
+ Dovecot is a mail server whose major goals are security and extreme
+ reliability. It tries very hard to handle all error conditions and verify
+ that all data is valid, making it nearly impossible to crash. It supports
+ mbox/Maildir and its own dbox/mdbox formats, and should also be pretty
+ fast, extensible, and portable.
+ .
+ This package contains the Dovecot ManageSieve server.
+
+Package: dovecot-pgsql
+Architecture: any
+Depends: dovecot-core (= ${binary:Version}), ${misc:Depends}, ${shlibs:Depends}
+Description: secure POP3/IMAP server - PostgreSQL support
+ Dovecot is a mail server whose major goals are security and extreme
+ reliability. It tries very hard to handle all error conditions and verify
+ that all data is valid, making it nearly impossible to crash. It supports
+ mbox/Maildir and its own dbox/mdbox formats, and should also be pretty
+ fast, extensible, and portable.
+ .
+ This package provides PostgreSQL support for Dovecot.
+
+Package: dovecot-mysql
+Architecture: any
+Depends: dovecot-core (= ${binary:Version}), ${misc:Depends}, ${shlibs:Depends}
+Description: secure POP3/IMAP server - MySQL support
+ Dovecot is a mail server whose major goals are security and extreme
+ reliability. It tries very hard to handle all error conditions and verify
+ that all data is valid, making it nearly impossible to crash. It supports
+ mbox/Maildir and its own dbox/mdbox formats, and should also be pretty
+ fast, extensible, and portable.
+ .
+ This package provides MySQL support for Dovecot.
+
+Package: dovecot-sqlite
+Architecture: any
+Depends: dovecot-core (= ${binary:Version}), ${misc:Depends}, ${shlibs:Depends}
+Description: secure POP3/IMAP server - SQLite support
+ Dovecot is a mail server whose major goals are security and extreme
+ reliability. It tries very hard to handle all error conditions and verify
+ that all data is valid, making it nearly impossible to crash. It supports
+ mbox/Maildir and its own dbox/mdbox formats, and should also be pretty
+ fast, extensible, and portable.
+ .
+ This package provides SQLite support for Dovecot.
+
+Package: dovecot-ldap
+Architecture: any
+Depends: dovecot-core (= ${binary:Version}),
+         ucf,
+         ${misc:Depends},
+         ${shlibs:Depends}
+Replaces: dovecot-core (<< 1:2.2.26.0-1~)
+Breaks: dovecot-core (<< 1:2.2.26.0-1~)
+Description: secure POP3/IMAP server - LDAP support
+ Dovecot is a mail server whose major goals are security and extreme
+ reliability. It tries very hard to handle all error conditions and verify
+ that all data is valid, making it nearly impossible to crash. It supports
+ mbox/Maildir and its own dbox/mdbox formats, and should also be pretty
+ fast, extensible, and portable.
+ .
+ This package provides LDAP support for Dovecot.
+
+Package: dovecot-gssapi
+Architecture: any
+Depends: dovecot-core (= ${binary:Version}), ${misc:Depends}, ${shlibs:Depends}
+Description: secure POP3/IMAP server - GSSAPI support
+ Dovecot is a mail server whose major goals are security and extreme
+ reliability. It tries very hard to handle all error conditions and verify
+ that all data is valid, making it nearly impossible to crash. It supports
+ mbox/Maildir and its own dbox/mdbox formats, and should also be pretty
+ fast, extensible, and portable.
+ .
+ This package provides GSSAPI authentication support for Dovecot.
+
+Package: dovecot-sieve
+Architecture: any
+Depends: dovecot-core (= ${binary:Version}),
+         ucf,
+         ${misc:Depends},
+         ${shlibs:Depends}
+Breaks: dovecot-core (<= 1:2.2.13-6), dovecot-imapd (<< 1:2.2.26.0-1~)
+Replaces: dovecot-core (<= 1:2.2.13-6), dovecot-imapd (<< 1:2.2.26.0-1~)
+Description: secure POP3/IMAP server - Sieve filters support
+ Dovecot is a mail server whose major goals are security and extreme
+ reliability. It tries very hard to handle all error conditions and verify
+ that all data is valid, making it nearly impossible to crash. It supports
+ mbox/Maildir and its own dbox/mdbox formats, and should also be pretty
+ fast, extensible, and portable.
+ .
+ This package provides Sieve filters support for Dovecot.
+
+Package: dovecot-flatcurve
+Architecture: any
+Conflicts: dovecot-fts-flatcurve
+Replaces: dovecot-fts-flatcurve
+Depends: dovecot-core (= ${binary:Version}),
+         ucf,
+         ${misc:Depends},
+         ${shlibs:Depends}
+Description: secure POP3/IMAP server - Flatcurve support
+ Dovecot is a mail server whose major goals are security and extreme
+ reliability. It tries very hard to handle all error conditions and verify
+ that all data is valid, making it nearly impossible to crash. It supports
+ mbox/Maildir and its own dbox/mdbox formats, and should also be pretty
+ fast, extensible, and portable.
+ .
+ This package provides Flatcurve full text search support for Dovecot.
+
+Package: dovecot-solr
+Architecture: any
+Depends: dovecot-core (= ${binary:Version}), ${misc:Depends}, ${shlibs:Depends}
+Description: secure POP3/IMAP server - Solr support
+ Dovecot is a mail server whose major goals are security and extreme
+ reliability. It tries very hard to handle all error conditions and verify
+ that all data is valid, making it nearly impossible to crash. It supports
+ mbox/Maildir and its own dbox/mdbox formats, and should also be pretty
+ fast, extensible, and portable.
+ .
+ This package provides Solr full text search support for Dovecot.
+
+Package: dovecot-submissiond
+Architecture: any
+Depends: dovecot-core (= ${binary:Version}),
+         ucf,
+         ${misc:Depends},
+         ${shlibs:Depends}
+Breaks: dovecot-submission (<< 1:2.3.1-2~)
+Replaces: dovecot-submission (<< 1:2.3.1-2~)
+Description: secure POP3/IMAP server - mail submission agent
+ Dovecot is a mail server whose major goals are security and extreme
+ reliability. It tries very hard to handle all error conditions and verify
+ that all data is valid, making it nearly impossible to crash. It supports
+ mbox/Maildir and its own dbox/mdbox formats, and should also be pretty
+ fast, extensible, and portable.
+ .
+ This package contains the Dovecot Mail Submission Agent which implements a
+ basic SMTP submission service with BURL support.
+
+Package: dovecot-auth-lua
+Architecture: any
+Depends: dovecot-core (= ${binary:Version}), ${misc:Depends}, ${shlibs:Depends}
+Description: secure POP3/IMAP server - Lua authentication plugin
+ Dovecot is a mail server whose major goals are security and extreme
+ reliability. It tries very hard to handle all error conditions and verify
+ that all data is valid, making it nearly impossible to crash. It supports
+ mbox/Maildir and its own dbox/mdbox formats, and should also be pretty
+ fast, extensible, and portable.
+ .
+ This package contains an authentication plugin allowing password and user
+ databases to be implemented in Lua.
diff --git a/copyright b/copyright
new file mode 100644 (file)
index 0000000..8ede253
--- /dev/null
+++ b/copyright
@@ -0,0 +1,266 @@
+Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+Upstream-Name: dovecot
+Upstream-Contact: Timo Sirainen <tss@iki.fi>
+                  Dovecot Mailing List <dovecot@dovecot.org>
+Comment: This package was debianized by Jaldhar H. Vyas <jaldhar@debian.org> on
+         Tue,  3 Dec 2002 01:10:07 -0500.
+Source: https://github.com/dovecot/core
+Files-Excluded-pigeonhole: doc/rfc
+
+Files: *
+Copyright: (c) 2001-2022 Dovecot authors
+Comment: see AUTHORS
+License: LGPL-2.1
+
+Files: src/lib/*
+       src/lib-sql/*
+Copyright: (c) 2001-2022 Dovecot authors
+Comment: see AUTHORS
+License: MIT
+
+Files: src/auth/*
+Copyright: (c) 2004-2006 Andrey Panin <pazke@donpac.ru>
+           (c) 2005 Jelmer Vernooij <jelmer@samba.org>
+           (c) 2011-2016 Florian Zeitz <florob@babelmonkeys.de>
+           (c) 2007 Dmitry Butskoy <dmitry@butskoy.name>
+           (C) 2006 Simon L Jackson
+           (c) 2015-2017 Dovecot Oy
+License: MIT
+
+Files: src/lib-auth/password-scheme-md5crypt.c
+Copyright: Poul-Henning Kamp <phk@login.dknet.dk>
+License: Beerware
+ "THE BEER-WARE LICENSE" (Revision 42):
+ <phk@login.dknet.dk> wrote this file.  As long as you retain this notice you
+ can do whatever you want with this stuff. If we meet some day, and you think
+ this stuff is worth it, you can buy me a beer in return.   Poul-Henning Kamp
+
+Files: src/lib-otp/*
+Copyright: (c) 2004-2006 Andrey Panin <pazke@donpac.ru>
+           (c) Copyright 1997-2005 Simon Tatham
+License: MIT
+
+Files: src/lib/md5.c
+Copyright: N/A
+License: public-domain-solardiz
+ Written by Solar Designer <solar@openwall.com> in 2001, and placed in the
+ public domain. There's absolutely no warrant.
+
+Files: src/lib/sha1.*
+Copyright:  Copyright (C) 1995, 1996, 1997, and 1998 WIDE Project.
+License: BSD-3-Clause
+
+Files: src/lib/sha2.* src/lib/sha3.*
+Copyright: Copyright (C) 2005, 2007 Olivier Gay <olivier.gay@a3.epfl.ch>
+License: BSD-3-Clause
+
+Files: src/lib/byteorder.h src/lib/test-byteorder.c
+Copyright: (c) 2016-2017 Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
+License: MIT
+
+Files: src/lib/hmac.*
+Copyright: (c) 2004 Andrey Panin <pazke@donpac.ru>
+           (c) 2011-2016 Florian Zeitz <florob@babelmonkeys.de>
+License: MIT
+
+Files: src/lib/hmac-cram-md5.c
+Copyright: (c) 2003 Joshua Goodall <joshua@roughtrade.net>
+License: MIT
+
+Files: src/lib/ioloop-iolist.c
+Copyright: (c) 2004 Andrey Panin <pazke@donpac.ru>
+License: MIT
+
+Files: src/lib/ioloop-kqueue.c src/lib/ioloop-notify-kqueue.c
+Copyright: (c) 2005 Vaclav Haisman <v.haisman@sh.cvut.cz>
+License: MIT
+
+Files: src/lib/UnicodeData.txt
+Copyright: Copyright (C) 1991-2007 Unicode, Inc.
+License: Unicode-DFS-2015
+
+Files: src/lib-language/stopwords/*
+Copyright: Copyright (c) 2001, Dr Martin Porter
+           Copyright (c) 2002, Richard Boulton.
+Comment: License obtained from http://snowball.tartarus.org/license.html
+License: BSD-3-Clause
+
+Files: src/lib-language/stopwords/stopwords_en.txt
+Copyright: Apache Software Foundation
+License: Apache-2.0
+
+Files: src/lib-language/stopwords/stopwords_ro.txt
+Copyright: (c) 2005, Jacques Savoy.
+License: BSD-2-Clause
+
+Files: debian/*
+Copyright: (c) 2006-2016 Jaldhar H. Vyas <jaldhar@debian.org>
+           (c) 2006-2009 Fabio Tranchitella <kobold@debian.org>
+           (c) 2008-2009 Joel Johnson <mrjoel@lixil.net>
+           (c) 2009-2012 Marco Nenciarini <mnencia@debian.org>
+           (c) 2011-2013 Micah Anderson <micah@debian.org>
+           (c) 2014-2015 Jelmer Vernooij <jelmer@debian.org>
+           (c) 2016-2018 Apollon Oikonomopoulos <apoikos@debian.org>
+          (c) 2020-2022 Noah Meyerhans <noahm@debian.org>
+          (c) 2020-2022 Christian Göttsche <cgzones@googlemail.com>
+License: GPL-2+
+
+Files: pigeonhole/*
+Copyright: (c) 2002-2021 Stephan Bosch <stephan@rename-it.nl>
+           (c) 2002-2021 Dovecot authors
+Comment: Source obtained from https://pigeonhole.dovecot.org/
+License: LGPL-2.1
+
+License: MIT
+ Permission is hereby granted, free of charge, to any person obtaining a
+ copy of this software and associated documentation files (the "Software"),
+ to deal in the Software without restriction, including without limitation
+ the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ and/or sell copies of the Software, and to permit persons to whom the
+ Software is furnished to do so, subject to the following conditions:
+ .
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+ .
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ DEALINGS IN THE SOFTWARE.
+
+License: LGPL-2.1
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+ .
+ This library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ Lesser General Public License for more details.
+ .
+ You should have received a copy of the GNU Lesser General Public
+ License along with this program.  If not, see
+ <https://www.gnu.org/licenses/>.
+ .
+ On Debian systems, the full text of the GNU Lesser General Public License can
+ be found at /usr/share/common-licenses/LGPL-2.1.
+
+License: Apache-2.0
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+ .
+     http://www.apache.org/licenses/LICENSE-2.0
+ .
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ .
+ On Debian systems, the full text of the Apache License, Version 2.0 can be
+ found at /usr/share/common-licenses/Apache-2.0.
+
+License: BSD-3-Clause
+ All rights reserved.
+ .
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+ 1. Redistributions of source code must retain the above copyright
+    notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+    notice, this list of conditions and the following disclaimer in the
+    documentation and/or other materials provided with the distribution.
+ 3. Neither the name of the University nor the names of its contributors
+    may be used to endorse or promote products derived from this software
+    without specific prior written permission.
+ .
+ THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ SUCH DAMAGE.
+
+License: BSD-2-Clause
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are met:
+ .
+ 1. Redistributions of source code must retain the above copyright notice, this
+    list of conditions and the following disclaimer.
+ .
+ 2. Redistributions in binary form must reproduce the above copyright notice,
+    this list of conditions and the following disclaimer in the documentation
+    and/or other materials provided with the distribution.
+ .
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+License: GPL-2+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+ .
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ GNU General Public License for more details.
+ .
+ You should have received a copy of the GNU General Public License
+ along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ .
+ On Debian systems, the full text of the GNU General Public License version 2
+ can be found at /usr/share/common-licenses/GPL-2.
+
+License: Unicode-DFS-2015
+ Copyright Â© 1991-2015 Unicode, Inc. All rights reserved. Distributed under the
+ Terms of Use in http://www.unicode.org/copyright.html.
+ .
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of the Unicode data files and any associated documentation (the "Data Files")
+ or Unicode software and any associated documentation (the "Software") to deal
+ in the Data Files or Software without restriction, including without limitation
+ the rights to use, copy, modify, merge, publish, distribute, and/or sell copies
+ of the Data Files or Software, and to permit persons to whom the Data Files or
+ Software are furnished to do so, provided that
+ .
+ (a) this copyright and permission notice appear with all copies of the Data
+     Files or Software,
+ (b) this copyright and permission notice appear in associated documentation,
+     and
+ (c) there is clear notice in each modified Data File or in the Software as
+     well as in the documentation associated with the Data File(s) or Software
+     that the data or software has been modified.
+ .
+ THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+ KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF THIRD
+ PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN
+ THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL
+ DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
+ WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
+ OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA FILES OR
+ SOFTWARE.
+ .
+ Except as contained in this notice, the name of a copyright holder shall not
+ be used in advertising or otherwise to promote the sale, use or other dealings
+ in these Data Files or Software without prior written authorization of the
+ copyright holder.
diff --git a/dh.pem b/dh.pem
new file mode 100644 (file)
index 0000000..a734b90
--- /dev/null
+++ b/dh.pem
@@ -0,0 +1,13 @@
+-----BEGIN DH PARAMETERS-----
+MIICCAKCAgEA///////////JD9qiIWjCNMTGYouA3BzRKQJOCIpnzHQCC76mOxOb
+IlFKCHmONATd75UZs806QxswKwpt8l8UN0/hNW1tUcJF5IW1dmJefsb0TELppjft
+awv/XLb0Brft7jhr+1qJn6WunyQRfEsf5kkoZlHs5Fs9wgB8uKFjvwWY2kg2HFXT
+mmkWP6j9JM9fg2VdI9yjrZYcYvNWIIVSu57VKQdwlpZtZww1Tkq8mATxdGwIyhgh
+fDKQXkYuNs474553LBgOhgObJ4Oi7Aeij7XFXfBvTFLJ3ivL9pVYFxg5lUl86pVq
+5RXSJhiY+gUQFXKOWoqqxC2tMxcNBFB6M6hVIavfHLpk7PuFBFjb7wqK6nFXXQYM
+fbOXD4Wm4eTHq/WujNsJM9cejJTgSiVhnc7j0iYa0u5r8S/6BtmKCGTYdgJzPshq
+ZFIfKxgXeyAMu+EXV3phXWx3CYjAutlG4gjiT6B05asxQ9tb/OD9EI5LgtEgqSEI
+ARpyPBKnh+bXiHGaEL26WyaZwycYavTiPBqUaDS2FQvaJYPpyirUTOjbu8LbBN6O
++S6O/BQfvsqmKHxZR05rwF2ZspZPoJDDoiM7oYZRW+ftH2EpcM7i16+4G912IXBI
+HNAGkSfVsFqpk7TqmI2P3cGG/7fckKbAj030Nck0BjGZ//////////8CAQI=
+-----END DH PARAMETERS-----
diff --git a/dovecot-core.NEWS b/dovecot-core.NEWS
new file mode 100644 (file)
index 0000000..c4e6a5f
--- /dev/null
@@ -0,0 +1,193 @@
+dovecot (1:2.4.1+dfsg1-1) unstable; urgency=medium
+
+  Dovecot 2.4 is a major upgrade from the previous 2.3 branch and introduces
+  an incompatible configuration language.  Please see
+  https://doc.dovecot.org/2.4.0/installation/upgrade/2.3-to-2.4.html for
+  details on transitioning to the new language.  The default configuration
+  shipped by Debian has been updated for compatibility with the new
+  configuration, but any locally maintained configuration will need to be
+  updated.
+
+ -- Noah Meyerhans <noahm@debian.org>  Mon, 10 Mar 2025 13:28:20 -0400
+
+dovecot (1:2.3.2-1) unstable; urgency=medium
+
+  Upgrading to the 2.3 series may require manual configuration changes.
+  Some settings have been removed, while others have had their defaults
+  changed. Please see
+
+    /usr/share/doc/dovecot-core/wiki/Upgrading.2.3.txt.gz
+
+  or the online version at
+
+    https://wiki2.dovecot.org/Upgrading/2.3
+
+  for more information and review your configuration accordingly.
+
+ -- Apollon Oikonomopoulos <apoikos@debian.org>  Sat, 24 Mar 2018 00:34:07 +0200
+
+dovecot (1:2.2.31-1) unstable; urgency=medium
+
+  TLS is now enabled by default, using the ssl-cert-snakeoil certificate
+  provided by the ssl-cert package. Upgrades from older versions will be
+  prompted to accept the new configuration and enable TLS. If you have already
+  configured TLS yourself, you'll most probably want to keep your settings
+  intact.
+ 
+  See /usr/share/doc/dovecot-core/README.Debian for more information on the
+  certificate's default location and how to install your own certificates.
+ 
+ -- Apollon Oikonomopoulos <apoikos@debian.org>  Sun, 25 Jun 2017 01:09:28 +0300
+
+dovecot (1:2.2.21-1) unstable; urgency=medium
+
+  This release disables the dovecot.socket systemd unit by default. The unit is
+  disabled only if the dovecot.service unit is already enabled, making sure
+  that dovecot will start on system boot. If you are upgrading dovecot and
+  previously relied on dovecot.socket and dovecot.service being both enabled,
+  please re-enable dovecot.socket manually using
+
+     systemctl enable dovecot.socket
+
+  Future package updates will not disable the socket unit again. For details
+  regarding this decision, please see Debian bugs #803915 and #814999.
+
+ -- Apollon Oikonomopoulos <apoikos@debian.org>  Fri, 19 Feb 2016 16:54:27 +0200
+
+dovecot (1:2.1.7-7) unstable; urgency=high
+
+  If you are upgrading from stable or the earlier 2.1.7 packages in testing,
+  you are advised to  backup your configuration files before continuing.
+  Carefully read /usr/share/doc/dovecot-core/README.Debian.gz for important
+  information about problems that might affect your upgrade.
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Mon, 04 Feb 2013 16:27:17 -0500
+
+dovecot (1:2.0.5-1) experimental; urgency=low
+
+  * A lot of settings have changed. Dovecot v2.0 can still use most of
+    the v1.x configuration files, but it logs a lot of warnings at
+    startup. A quick and easy way to convert your old config file to
+    v2.0 format is:
+
+    doveconf -n -c dovecot-1.conf > dovecot-2.conf
+
+    This command logs a warning about each obsolete setting it
+    converts to the new format. You can either go through the warnings
+    to figure out what changes exactly were done, or you can simply
+    trust doveconf and replace your old config with the newly
+    generated one.
+
+  * Dovecot uses two system users for internal purposes now by
+    default: "dovenull" and "dovecot". "dovenull" user is used by
+    completely untrustworthy processes, while "dovecot" user is used
+    for slightly more trusted processes.
+
+  * In conf.d/10-mail.conf, the default mail_location is set to
+    mbox:~/mail:INBOX=/var/mail/%u
+    This has to be set because Dovecots auto-detection of mailboxes fails
+    when a user has no mail.  I tried to provide a reasonable default
+    but advanced users will certainly want to change this.
+
+    Related to this, you might see an error in the logs about dovecot not
+    being able to chgrp certain files.  This is because the default user
+    mailboxes in Debian belong to group mail which dovecot is not part of
+    (nor should be.)  The Debian package has been patched so that the error
+    will not be fatal but for a more permanent solution, see here:
+    http://wiki2.dovecot.org/Errors/ChgrpNoPerm  for some discussion.  Or
+    you can set mail_location (and the equivalent settings in your MDA) so
+    that mailboxes are in the users home directory.
+
+  * No more convert plugin, use dsync instead
+
+  * No more expire-tool, use doveadm expunge instead. Also expire
+    configuration is different.
+
+  * Post-login scripts are configured differently and need to be modified
+
+  * Quota warnings are configured differently and the script may need
+    to be modified (most environment settings like $USER are gone)
+
+  * Global ACL filenames now require namespace prefix (e.g. if you use
+    "INBOX." prefix, /etc/acls/foo needs to be renamed to
+    /etc/acls/INBOX.foo
+
+  * Maildir: Permissions for newly created mail files are no longer
+    copied from dovecot-shared file, but instead from the mail
+    directory (e.g. for "foo" mailbox, they're taken from
+    ~/Maildir/.foo directory)
+
+  * dbox: v2.0 format is slightly different, but backwards
+    compatible. The main problem is that v2.0 no longer supports
+    maildir-dbox hybrid resulting from "fast Maildir migration". If
+    you have any Maildir files in your dbox, you need to convert them
+    somehow (some examples). You might also consider using dsync to
+    get rid of the old unused metadata in your dbox files.
+
+
+  * Pre-login and post-login CAPABILITY reply is now
+    different. Dovecot expects clients to recognize new automatically
+    sent capabilities. This should work with all commonly used
+    clients, but some rarely used clients might have problems. Either
+    get the client fixed, or set imap_capability manually.
+
+  * ManageSieve protocol was assigned an official port by IANA:
+    4190. This is used by Pigeonhole by default now. If you want to
+    listen also on the old 2000 port, see the example in
+    /usr/share/doc/dovecot-common/wiki/ManageSieve.Configuration.txt.gz
+
+  * deliver binary was renamed to dovecot-lda (but a symlink still
+    exists for now)
+
+  * deliver -n parameter was replaced by lda_mailbox_autocreate
+    setting. The default also changed to "no".
+
+  * deliver -s parameter was replaced by lda_mailbox_autosubscribe
+    setting. The default is "no", as before.
+
+ -- Marco Nenciarini <mnencia@debian.org>  Mon, 18 Oct 2010 12:59:14 +0200
+
+dovecot (1:1.0.12-1) unstable; urgency=low
+
+  * mail_extra_groups=mail setting is often used insecurely to give Dovecot
+    access to create dotlocks to /var/mail directory. The setting has been
+    replaced by mail_privileged_group and mail_access_groups.
+    Read also: http://dovecot.org/list/dovecot/2008-March/029196.html
+
+ -- Fabio Tranchitella <kobold@debian.org>  Thu, 06 Mar 2008 15:46:03 +0100
+
+dovecot (1.0.beta3-1) unstable; urgency=low
+
+  * Starting from this release, dovecot-lda is included in dovecot-common.
+    The previous executable deliver has been removed from the upstream 
+    source package.
+  * Other new features recently added include
+    + vpopmail support
+    + quota support
+    + GSSAPI support
+  * All these new features mean there are some configuration file changes 
+    please review the default /etc/dovecot/dovecot.conf and merge in any
+    new bits.  (If you don't use any new features, your configuration should
+    remain compatible.)
+
+ -- Fabio Tranchitella <kobold@debian.org>  Fri, 17 Feb 2006 17:05:37 +0000
+
+dovecot (1.0.alpha4-1) unstable; urgency=low
+
+  * This is the 1.0alpha branch.  Once again there have been incompatible 
+    changes to the syntax of /etc/dovecot/dovecot.conf. 
+  * The dovecot-common package now includes the zlib plugin for compressed
+    mboxen.  Make sure you have mail_use_modules=yes in imap and/or pop3 
+    section in /etc/dovecot/dovecot.conf if you want to use this.
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Wed, 20 Jul 2005 06:30:37 -0400
+
+dovecot (0.99.20050712-1) unstable; urgency=low
+
+  * This is the 1.0stable development branch.  There have been major 
+    changes and new features have been added so check your configuration 
+    carefully.  In particular, /etc/dovecot/dovecot-mysql.conf and 
+    /etc/dovecot/dovecot-pgsql.conf have been replaced by
+    /etc/dovecot/dovecot-sql.conf .
+
+ -- Jaldhar H. Vyas <jaldhar@debian.org>  Wed, 20 Jul 2005 06:30:37 -0400
diff --git a/dovecot-core.README.Debian b/dovecot-core.README.Debian
new file mode 100644 (file)
index 0000000..e4fc73c
--- /dev/null
@@ -0,0 +1,35 @@
+Dovecot Debian package
+======================
+
+Please refer to http://wiki2.dovecot.org for the full documentation of this
+package and its configuration.
+
+Note that the default value for mbox_write_locks differs between Debian and
+upstream Dovecot:
+       Debian:  mbox_write_locks = "fnctl dotlock"
+       Dovecot: mbox_write_locks = "dotlock fnctl"
+
+TLS setup
+---------
+
+TLS is enabled by default on new installations. Dovecot will use the
+self-signed certificate provided by ssl-cert-snakeoil by default. The
+certificate and key are accessed via symbolic links in /etc/dovecot/private.
+
+If you wish to use your own certificates, feel free to replace the symbolic
+links in /etc/dovecot/private with the actual certificate/key of your
+preference, or place the certificate and key at the location of your preference
+and update /etc/dovecot/conf.d/10-ssl.conf accordingly.
+
+dovenull group
+--------------
+
+Upstream recommends that for security's sake, the dovenull user should belong
+to its own private group to which no one else belongs.  Prior to 2.2.9-1, the
+dovenull user was in the nogroup group which contains other users.  This has
+been rectified for new installs but upgraders should make the change
+themselves like this:
+
+# addgroup --system --group dovenull
+# usermod -g dovenull dovenull
+
diff --git a/dovecot-core.bug-control b/dovecot-core.bug-control
new file mode 100644 (file)
index 0000000..0002beb
--- /dev/null
@@ -0,0 +1 @@
+package-status: dovecot-common dovecot-core dovecot-dev dovecot-gssapi dovecot-imapd dovecot-ldap dovecot-lmtpd dovecot-managesieved dovecot-mysql dovecot-pgsql dovecot-pop3d dovecot-sieve dovecot-sqlite
diff --git a/dovecot-core.bug-script b/dovecot-core.bug-script
new file mode 100644 (file)
index 0000000..1bf1d19
--- /dev/null
@@ -0,0 +1,28 @@
+#!/bin/bash
+
+set -e
+
+pecho() {
+    echo "$@"
+    echo "$@"|sed 's/./-/g'
+}
+
+
+if [ -x /usr/bin/doveconf ]
+then
+       echo
+       cat <<-EOF
+       You can append "doveconf -n" output to the bug report. It will help the
+       maintainers to better understand your configuration.
+       It normally doesn't contain any sensitive information, but you should 
+        review it before sending.
+       EOF
+       echo
+       yesno "Do you want to append \"doveconf -n\" output to your bug report? " yep
+       if [ "$REPLY" == yep ]
+       then
+               echo >&3
+               pecho "dovecot configuration" >&3
+               /usr/bin/doveconf -n >&3
+       fi
+fi
diff --git a/dovecot-core.dirs b/dovecot-core.dirs
new file mode 100644 (file)
index 0000000..b91716f
--- /dev/null
@@ -0,0 +1,4 @@
+etc/dovecot/conf.d
+etc/dovecot/private
+usr/share/dovecot/protocols.d
+var/lib/dovecot
diff --git a/dovecot-core.docs b/dovecot-core.docs
new file mode 100644 (file)
index 0000000..166765a
--- /dev/null
@@ -0,0 +1,2 @@
+NEWS
+TODO
diff --git a/dovecot-core.dovecot.default b/dovecot-core.dovecot.default
new file mode 100644 (file)
index 0000000..dee9239
--- /dev/null
@@ -0,0 +1,4 @@
+# /etc/default/dovecot
+
+# Set to '1' to allow Dovecot daemons to produce core dumps
+#ALLOW_COREDUMPS=1
diff --git a/dovecot-core.dovecot.init b/dovecot-core.dovecot.init
new file mode 100644 (file)
index 0000000..730c323
--- /dev/null
@@ -0,0 +1,185 @@
+#! /bin/sh
+### BEGIN INIT INFO
+# Provides:          dovecot
+# Required-Start:    $local_fs $remote_fs $network $syslog $time
+# Required-Stop:     $local_fs $remote_fs $network $syslog
+# Should-Start:      postgresql mysql slapd winbind nslcd
+# Should-Stop:       postgresql mysql slapd winbind nslcd
+# Default-Start:     2 3 4 5
+# Default-Stop:      0 1 6
+# Short-Description: Dovecot init script
+# Description:       Init script for dovecot services
+### END INIT INFO
+
+# Author: Miquel van Smoorenburg <miquels@cistron.nl>.
+#         Modified for Debian GNU/Linux
+#         by Ian Murdock <imurdock@gnu.ai.mit.edu>.
+#
+
+# Do NOT "set -e"
+
+# PATH should only include /usr/* if it runs after the mountnfs.sh script
+PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
+DESC="IMAP/POP3 mail server"
+NAME=dovecot
+DAEMON=/usr/sbin/dovecot
+DAEMON_ARGS=""
+SCRIPTNAME=/etc/init.d/$NAME
+CONF=/etc/dovecot/${NAME}.conf
+
+# Read configuration variable file if it is present
+[ -r /etc/default/$NAME ] && . /etc/default/$NAME
+
+# Exit if the package is not installed
+[ -x "$DAEMON" ] || exit 0
+
+# Exit if the configuration file doesn't exist
+[ -f "$CONF" ] || exit 0
+
+# Exit if explicitly told to
+[ "$ENABLED" != "0" ] || exit 0
+
+# Allow core dumps if requested
+[ "$ALLOW_COREDUMPS" != "1" ] || ulimit -c unlimited
+
+# Define LSB log_* functions.
+# Depend on lsb-base (>= 3.0-6) to ensure that this file is present.
+. /lib/lsb/init-functions
+
+# conf file readable?
+if [ ! -r ${CONF} ]; then
+  log_daemon_msg "${CONF}: not readable" "$NAME" && log_end_msg 1;
+  exit 1;
+fi
+
+# dont check for inetd.conf if its not installed
+if [ -f /etc/inetd.conf ]; then
+  # The init script should do nothing if dovecot or another imap/pop3 server
+  # is being run from inetd, and dovecot is configured to run as an imap or
+  # pop3 service
+  for p in `sed -r "s/^ *(([^:]+|\[[^]]+]|\*):)?(pop3s?|imaps?)[ \t].*/\3/;t;d" \
+    /etc/inetd.conf`
+  do
+    for q in `doveconf -n -h protocols`
+    do
+      if [ $p = $q ]; then
+        log_daemon_msg "protocol ${p} configured both in inetd and in dovecot" "$NAME" && log_end_msg 1
+        exit 0
+      fi
+    done
+  done
+fi
+
+# determine the location of the PID file
+# override by setting base_dir in conf file or PIDBASE in /etc/defaults/$NAME
+PIDBASE=${PIDBASE:-`doveconf -n -c ${CONF} -h base_dir`}
+PIDFILE=${PIDBASE:-/var/run/dovecot}/master.pid
+
+#
+# Function that starts the daemon/service
+#
+do_start()
+{
+    # Return
+    #   0 if daemon has been started
+    #   1 if daemon was already running
+    #   2 if daemon could not be started
+    start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON --test -- -c ${CONF} > /dev/null \
+        || return 1
+    start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON -- -c ${CONF} \
+        $DAEMON_ARGS \
+        || return 2
+}
+
+#
+# Function that stops the daemon/service
+#
+do_stop()
+{
+    # Return
+    #   0 if daemon has been stopped
+    #   1 if daemon was already stopped
+    #   2 if daemon could not be stopped
+    #   other if a failure occurred
+    start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE --name ${DAEMON##*/}
+    RETVAL="$?"
+    [ "$RETVAL" = 2 ] && return 2
+    # Wait for children to finish too if this is a daemon that forks
+    # and if the daemon is only ever run from this initscript.
+    # If the above conditions are not satisfied then add some other code
+    # that waits for the process to drop all resources that could be
+    # needed by services started subsequently.  A last resort is to
+    # sleep for some time.
+    start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --pidfile $PIDFILE --name ${DAEMON##*/}
+    [ "$?" = 2 ] && return 2
+    # Many daemons don't delete their pidfiles when they exit.
+    rm -f $PIDFILE
+    return "$RETVAL"
+}
+
+#
+# Function that sends a SIGHUP to the daemon/service
+#
+do_reload() {
+    #
+    # If the daemon can reload its configuration without
+    # restarting (for example, when it is sent a SIGHUP),
+    # then implement that here.
+    #
+    start-stop-daemon --stop --signal HUP --quiet --pidfile $PIDFILE --name $NAME
+    return 0
+}
+
+
+case "$1" in
+  start)
+    log_daemon_msg "Starting $DESC" "$NAME"
+    do_start
+    case "$?" in
+        0|1) log_end_msg 0 ;;
+        2) log_end_msg 1 ;;
+    esac
+    ;;
+  stop)
+    log_daemon_msg "Stopping $DESC" "$NAME"
+    do_stop
+    case "$?" in
+        0|1) log_end_msg 0 ;;
+        2) log_end_msg 1 ;;
+    esac
+    ;;
+  reload|force-reload)
+    log_daemon_msg "Reloading $DESC" "$NAME"
+    do_reload
+    log_end_msg $?
+    ;;
+  restart)
+    #
+    # If the "reload" option is implemented then remove the
+    # 'force-reload' alias
+    #
+    log_daemon_msg "Restarting $DESC" "$NAME"
+    do_stop
+    case "$?" in
+      0|1)
+        do_start
+        case "$?" in
+            0) log_end_msg 0 ;;
+            1) log_end_msg 1 ;; # Old process is still running
+            *) log_end_msg 1 ;; # Failed to start
+        esac
+        ;;
+      *)
+        # Failed to stop
+        log_end_msg 1
+        ;;
+    esac
+    ;;
+  status)
+    status_of_proc -p $PIDFILE $DAEMON $NAME && exit 0 || exit $?
+    ;;
+  *)
+    echo "Usage: $SCRIPTNAME {start|stop|restart|force-reload|status}" >&2
+    exit 3
+    ;;
+esac
diff --git a/dovecot-core.dovecot.pam b/dovecot-core.dovecot.pam
new file mode 100644 (file)
index 0000000..3443434
--- /dev/null
@@ -0,0 +1,6 @@
+#%PAM-1.0
+
+@include common-auth
+@include common-account
+@include common-session
+
diff --git a/dovecot-core.install b/dovecot-core.install
new file mode 100644 (file)
index 0000000..bbbaf19
--- /dev/null
@@ -0,0 +1,3 @@
+debian/dh.pem                          usr/share/dovecot
+debian/dovecot-openssl.cnf             usr/share/dovecot
+debian/maildirmake.dovecot             usr/bin
diff --git a/dovecot-core.lintian-overrides b/dovecot-core.lintian-overrides
new file mode 100644 (file)
index 0000000..f0c74da
--- /dev/null
@@ -0,0 +1,59 @@
+# hardening flags are set and blhc succeeds; might need some investigation
+dovecot-core: hardening-no-fortify-functions [usr/lib/dovecot/config]
+dovecot-core: hardening-no-fortify-functions [usr/lib/dovecot/gdbhelper]
+dovecot-core: hardening-no-fortify-functions [usr/lib/dovecot/libdovecot-storage.so.0.0.0]
+dovecot-core: hardening-no-fortify-functions [usr/lib/dovecot/modules/lib20_fts_plugin.so]
+dovecot-core: hardening-no-fortify-functions [usr/lib/dovecot/script-login]
+dovecot-core: hardening-no-fortify-functions [usr/lib/dovecot/script]
+dovecot-core: hardening-no-fortify-functions [usr/lib/dovecot/xml2text]
+dovecot-core: package-contains-empty-directory [usr/share/dovecot/protocols.d/]
+dovecot-core: package-contains-empty-directory [usr/lib/dovecot/modules/dict/]
+dovecot-core: spelling-error-in-readme-debian dovenull dovenull (duplicate word) dovenull [usr/share/doc/dovecot-core/README.Debian]
+dovecot-core: library-not-linked-against-libc [usr/lib/dovecot/modules/old-stats/libold_stats_mail.so]
+dovecot-core: library-not-linked-against-libc [usr/lib/dovecot/modules/old-stats/libstats_auth.so]
+dovecot-core [armel]: library-not-linked-against-libc usr/lib/dovecot/modules/lib20_listescape_plugin.so
+# ignore internal libraries without dependency information
+dovecot-core: shared-library-lacks-prerequisites [usr/lib/dovecot/modules/lib20_listescape_plugin.so]
+dovecot-core: shared-library-lacks-prerequisites [usr/lib/dovecot/modules/settings/libpigeonhole_settings.so]
+# ignore extra man pages
+dovecot-core: spare-manual-page [usr/share/man/man1/deliver.1.gz]
+# keep all man pages in section 1 for now
+dovecot-core: manual-page-for-system-command [usr/sbin/dovecot]
+# ignore executable-in-usr-lib, maybe move to libexec at some point
+dovecot-core: executable-in-usr-lib [usr/lib/dovecot/anvil]
+dovecot-core: executable-in-usr-lib [usr/lib/dovecot/auth]
+dovecot-core: executable-in-usr-lib [usr/lib/dovecot/config]
+dovecot-core: executable-in-usr-lib [usr/lib/dovecot/dict]
+dovecot-core: executable-in-usr-lib [usr/lib/dovecot/dns-client]
+dovecot-core: executable-in-usr-lib [usr/lib/dovecot/doveadm-server]
+dovecot-core: executable-in-usr-lib [usr/lib/dovecot/dovecot-lda]
+dovecot-core: executable-in-usr-lib [usr/lib/dovecot/gdbhelper]
+dovecot-core: executable-in-usr-lib [usr/lib/dovecot/health-check.sh]
+dovecot-core: executable-in-usr-lib [usr/lib/dovecot/indexer]
+dovecot-core: executable-in-usr-lib [usr/lib/dovecot/indexer-worker]
+dovecot-core: executable-in-usr-lib [usr/lib/dovecot/log]
+dovecot-core: executable-in-usr-lib [usr/lib/dovecot/quota-status]
+dovecot-core: executable-in-usr-lib [usr/lib/dovecot/rawlog]
+dovecot-core: executable-in-usr-lib [usr/lib/dovecot/script]
+dovecot-core: executable-in-usr-lib [usr/lib/dovecot/script-login]
+dovecot-core: executable-in-usr-lib [usr/lib/dovecot/stats]
+dovecot-core: executable-in-usr-lib [usr/lib/dovecot/xml2text]
+# ignore equal stub files
+dovecot-core: duplicate-files usr/share/doc/dovecot-core/wiki/Pigeonhole.ManageSieve.Install.txt usr/share/doc/dovecot-core/wiki/Plugins.Stats.txt
+# ignore stopwords txt files not in /usr/share/doc
+dovecot-core: package-contains-documentation-outside-usr-share-doc [usr/share/dovecot/stopwords/stopwords_da.txt]
+dovecot-core: package-contains-documentation-outside-usr-share-doc [usr/share/dovecot/stopwords/stopwords_de.txt]
+dovecot-core: package-contains-documentation-outside-usr-share-doc [usr/share/dovecot/stopwords/stopwords_en.txt]
+dovecot-core: package-contains-documentation-outside-usr-share-doc [usr/share/dovecot/stopwords/stopwords_es.txt]
+dovecot-core: package-contains-documentation-outside-usr-share-doc [usr/share/dovecot/stopwords/stopwords_fi.txt]
+dovecot-core: package-contains-documentation-outside-usr-share-doc [usr/share/dovecot/stopwords/stopwords_fr.txt]
+dovecot-core: package-contains-documentation-outside-usr-share-doc [usr/share/dovecot/stopwords/stopwords_it.txt]
+dovecot-core: package-contains-documentation-outside-usr-share-doc [usr/share/dovecot/stopwords/stopwords_nl.txt]
+dovecot-core: package-contains-documentation-outside-usr-share-doc [usr/share/dovecot/stopwords/stopwords_no.txt]
+dovecot-core: package-contains-documentation-outside-usr-share-doc [usr/share/dovecot/stopwords/stopwords_pt.txt]
+dovecot-core: package-contains-documentation-outside-usr-share-doc [usr/share/dovecot/stopwords/stopwords_ro.txt]
+dovecot-core: package-contains-documentation-outside-usr-share-doc [usr/share/dovecot/stopwords/stopwords_ru.txt]
+dovecot-core: package-contains-documentation-outside-usr-share-doc [usr/share/dovecot/stopwords/stopwords_sv.txt]
+dovecot-core: package-contains-documentation-outside-usr-share-doc [usr/share/dovecot/stopwords/stopwords_tr.txt]
+# ulimit -c might be specified but is supported by bash and dash
+dovecot-core: bash-term-in-posix-shell '| ulimit' [etc/init.d/dovecot:43]
diff --git a/dovecot-core.manpages b/dovecot-core.manpages
new file mode 100644 (file)
index 0000000..926f5cc
--- /dev/null
@@ -0,0 +1 @@
+debian/maildirmake.dovecot.1
diff --git a/dovecot-core.postinst b/dovecot-core.postinst
new file mode 100644 (file)
index 0000000..1ce4d49
--- /dev/null
@@ -0,0 +1,95 @@
+#!/bin/sh
+set -e
+
+if [ "$1" = "configure" ]; then
+  CONFFILES="dovecot.conf \
+    conf.d/10-auth.conf \
+    conf.d/10-logging.conf \
+    conf.d/10-mail.conf \
+    conf.d/10-master.conf \
+    conf.d/10-metrics.conf \
+    conf.d/10-ssl.conf \
+    conf.d/15-lda.conf \
+    conf.d/15-mailboxes.conf \
+    conf.d/30-dict-server.conf \
+    conf.d/90-acl.conf \
+    conf.d/90-fts.conf \
+    conf.d/90-quota.conf \
+    conf.d/auth-deny.conf.ext \
+    conf.d/auth-master.conf.ext \
+    conf.d/auth-oauth2.conf.ext \
+    conf.d/auth-passwdfile.conf.ext \
+    conf.d/auth-sql.conf.ext \
+    conf.d/auth-static.conf.ext \
+    conf.d/auth-system.conf.ext"
+
+  OLD_CONFFILES="conf.d/auth-vpopmail.conf.ext \
+    dovecot-dict-auth.conf.ext \
+    dovecot-dict-sql.conf.ext \
+    dovecot-sql.conf.ext \
+    conf.d/10-director.conf \
+    conf.d/10-tcpwrapper.conf \
+    conf.d/90-plugin.conf"
+
+  ## Users
+  #
+
+  adduser --system --group --home /usr/lib/dovecot --gecos "Dovecot mail server" \
+          --no-create-home --disabled-password --quiet dovecot || true
+
+  adduser --system --group --home /nonexistent --no-create-home --gecos "Dovecot login user" \
+          --disabled-password --quiet dovenull || true
+
+  for conffile in $CONFFILES ; do
+    # Tell ucf that the file in /usr/share/dovecot is the latest
+    # maintainer version, and let it handle how to manage the real
+    # configuration file in /etc/dovecot.
+    ucf --three-way "/usr/share/dovecot/$conffile" "/etc/dovecot/$conffile"
+    ucfr dovecot-core "/etc/dovecot/$conffile"
+    if [ "$conffile" != "dovecot.conf" ] && [ -f "/etc/dovecot/$conffile" ] &&
+               [ "$(echo "$conffile" | cut -b -7)" != "conf.d/" ]; then
+      chmod 0640 "/etc/dovecot/$conffile"
+      chgrp dovecot "/etc/dovecot/$conffile"
+    fi
+  done
+
+  for oldconffile in $OLD_CONFFILES ; do
+    if [ -e "/etc/dovecot/$oldconffile" ]; then
+      modified=$(ucfq -w /etc/dovecot/$oldconffile | cut -d: -f4)
+      if [ "$modified" = "No" ]; then
+       echo "Removing unmodified obsolete configuration file '/etc/dovecot/$oldconffile'"
+       rm -f "/etc/dovecot/$oldconffile"
+      else
+       echo "Locally modified configuration file '/etc/dovecot/$oldconffile' is obsolete. Please remove."
+      fi
+      ucf --purge "/etc/dovecot/$oldconffile"
+      ucfr --purge dovecot-core "/etc/dovecot/$oldconffile"
+    fi
+  done
+
+
+  if ! dpkg-statoverride --list /etc/dovecot/private >/dev/null; then
+    dpkg-statoverride --quiet --update --add root root 0700 /etc/dovecot/private
+  fi
+
+  # SSL configuration
+  # Use the ssl-cert-snakeoil certificate in the following cases:
+  # - On new installations
+  if [ -z "$2" ]; then
+    if [ ! -e /etc/dovecot/private/dovecot.key ] && \
+       [ ! -e /etc/dovecot/private/dovecot.pem ] && \
+       [ -e /etc/ssl/certs/ssl-cert-snakeoil.pem ] && \
+       [ -e /etc/ssl/private/ssl-cert-snakeoil.key ]; then
+      ln -s /etc/ssl/certs/ssl-cert-snakeoil.pem /etc/dovecot/private/dovecot.pem
+      ln -s /etc/ssl/private/ssl-cert-snakeoil.key /etc/dovecot/private/dovecot.key
+    fi
+  fi
+fi
+
+if [ "$1" = "triggered" ]; then
+    if [ -x "/etc/init.d/dovecot" ]; then
+        invoke-rc.d dovecot restart
+    fi
+fi
+
+#DEBHELPER#
diff --git a/dovecot-core.postrm b/dovecot-core.postrm
new file mode 100644 (file)
index 0000000..87f6d12
--- /dev/null
@@ -0,0 +1,48 @@
+#!/bin/sh
+set -e
+
+if [ "$1" = "purge" ] ; then
+    for conffile in $(ucfq --with-colons dovecot-core | cut -d: -f1); do
+        # we mimic dpkg as closely as possible, so we remove configuration
+        # files with dpkg backup extensions too:
+        ### Some of the following is from Tore Anderson:
+        for ext in '~' '%' .bak .dpkg-tmp .dpkg-new .dpkg-old .dpkg-dist .ucf-new .ucf-old .ucf-dist;  do
+            rm -f "$conffile$ext"
+        done
+        # remove the configuration file itself
+        rm -f "$conffile"
+        # and finally clear it out from the ucf database
+        if which ucf >/dev/null; then
+            ucf --purge "$conffile"
+        fi
+        if which ucfr >/dev/null; then
+            ucfr --purge dovecot-core "$conffile"
+        fi
+    done
+
+    userdel dovecot || true;
+    userdel dovenull || true;
+
+    if [ -d /run/dovecot ]; then
+        rm -rf /run/dovecot
+    fi
+
+    # Remove dovecot.pem and dovecot.key only if they are symlinks; otherwise
+    # we might remove CA-issued certificates that are difficult and/or
+    # expensive to replace.
+    for file in /etc/dovecot/private/dovecot.pem /etc/dovecot/private/dovecot.key; do
+        if [ -h "$file" ]; then
+            rm -f "$file"
+        else
+            echo "${file} is not a symbolic link, skipping removal." >&2
+        fi
+    done
+fi
+
+if [ "$1" = "triggered" ]; then
+    if [ -x "/etc/init.d/dovecot" ]; then
+        invoke-rc.d dovecot start
+    fi
+fi
+
+#DEBHELPER#
diff --git a/dovecot-core.triggers b/dovecot-core.triggers
new file mode 100644 (file)
index 0000000..9f8f2ff
--- /dev/null
@@ -0,0 +1,2 @@
+interest-noawait register-dovecot-plugin
+interest-noawait /usr/lib/dovecot/modules
diff --git a/dovecot-dev.README.Debian b/dovecot-dev.README.Debian
new file mode 100644 (file)
index 0000000..cdcf38f
--- /dev/null
@@ -0,0 +1,10 @@
+/usr/share/dovecot/dovecot-abi contains that portion of DOVECOT_ABI_VERSION
+from configure.ac outside parentheses (i.e not including the package version.)
+It might look something like this:
+
+2.2.ABIv10
+
+...packages providing plugins for dovecot can use this information in their
+dependencies to avoid having to update every time there is a minor update to 
+the dovecot package.
+
diff --git a/dovecot-dev.lintian-overrides b/dovecot-dev.lintian-overrides
new file mode 100644 (file)
index 0000000..1d459c2
--- /dev/null
@@ -0,0 +1,2 @@
+# plugins probably need the package name
+dovecot-dev: package-name-defined-in-config-h usr/include/dovecot/config.h
diff --git a/dovecot-flatcurve.links b/dovecot-flatcurve.links
new file mode 100644 (file)
index 0000000..6d5bbee
--- /dev/null
@@ -0,0 +1 @@
+/usr/share/bug/dovecot-core /usr/share/bug/dovecot-flatcurve
diff --git a/dovecot-flatcurve.postinst b/dovecot-flatcurve.postinst
new file mode 100644 (file)
index 0000000..1a1a324
--- /dev/null
@@ -0,0 +1,21 @@
+#!/bin/sh
+
+set -e
+
+if [ "$1" = "configure" ]; then
+  CONFFILES="conf.d/90-fts-flatcurve.conf"
+
+  for conffile in $CONFFILES ; do
+    # Tell ucf that the file in /usr/share/dovecot is the latest
+    # maintainer version, and let it handle how to manage the real
+    # configuration file in /etc/dovecot.
+    ucf --three-way "/usr/share/dovecot/$conffile" "/etc/dovecot/$conffile"
+    ucfr dovecot-ldap "/etc/dovecot/$conffile"
+    if [ "$conffile" != "dovecot.conf" ] && [ -f "/etc/dovecot/$conffile" ] &&
+               [ "$(echo "$conffile" | cut -b -7)" != "conf.d/" ]; then
+      chmod 0600 "/etc/dovecot/$conffile"
+    fi
+  done
+fi
+
+#DEBHELPER#
diff --git a/dovecot-flatcurve.postrm b/dovecot-flatcurve.postrm
new file mode 100644 (file)
index 0000000..c0cddcd
--- /dev/null
@@ -0,0 +1,25 @@
+#!/bin/sh
+set -e
+
+if [ "$1" = "purge" ] ; then
+       for conffile in $(ucfq --with-colons dovecot-flatcurve | cut -d: -f1); do
+               # we mimic dpkg as closely as possible, so we remove configuration
+               # files with dpkg backup extensions too:
+               ### Some of the following is from Tore Anderson:
+               for ext in '~' '%' .bak .dpkg-tmp .dpkg-new .dpkg-old .dpkg-dist .ucf-new .ucf-old .ucf-dist;  do
+                       rm -f "$conffile$ext"
+               done
+               # remove the configuration file itself
+               rm -f "$conffile"
+               # and finally clear it out from the ucf database
+               if which ucf >/dev/null; then
+                       ucf --purge "$conffile"
+               fi
+               if which ucfr >/dev/null; then
+                       ucfr --purge dovecot-flatcurve "$conffile"
+               fi
+       done
+
+fi
+
+#DEBHELPER#
diff --git a/dovecot-flatcurve.prerm b/dovecot-flatcurve.prerm
new file mode 100644 (file)
index 0000000..2ad0a1f
--- /dev/null
@@ -0,0 +1,12 @@
+#!/bin/sh
+set -e
+
+if [ -d /run/systemd/system ]; then
+       deb-systemd-invoke stop dovecot.socket || true
+fi
+
+if [ -x "/etc/init.d/dovecot" ]; then
+       invoke-rc.d dovecot stop
+fi
+
+#DEBHELPER#
diff --git a/dovecot-flatcurve.triggers b/dovecot-flatcurve.triggers
new file mode 100644 (file)
index 0000000..903b9e7
--- /dev/null
@@ -0,0 +1,2 @@
+activate-noawait register-dovecot-plugin
+
diff --git a/dovecot-gssapi.links b/dovecot-gssapi.links
new file mode 100644 (file)
index 0000000..dde94f9
--- /dev/null
@@ -0,0 +1 @@
+/usr/share/bug/dovecot-core /usr/share/bug/dovecot-gssapi
diff --git a/dovecot-imapd.links b/dovecot-imapd.links
new file mode 100644 (file)
index 0000000..d0fb029
--- /dev/null
@@ -0,0 +1 @@
+/usr/share/bug/dovecot-core /usr/share/bug/dovecot-imapd
diff --git a/dovecot-imapd.lintian-overrides b/dovecot-imapd.lintian-overrides
new file mode 100644 (file)
index 0000000..dc26e0d
--- /dev/null
@@ -0,0 +1,2 @@
+# hardening flags are set and blhc succeeds; might need some investigation
+dovecot-imapd: hardening-no-fortify-functions [usr/lib/dovecot/imap]
diff --git a/dovecot-imapd.postinst b/dovecot-imapd.postinst
new file mode 100644 (file)
index 0000000..1b134b6
--- /dev/null
@@ -0,0 +1,23 @@
+#!/bin/sh
+
+set -e
+
+if [ "$1" = "configure" ]; then
+  CONFFILES="conf.d/20-imap.conf"
+
+  for conffile in $CONFFILES ; do
+    # Tell ucf that the file in /usr/share/dovecot is the latest
+    # maintainer version, and let it handle how to manage the real
+    # configuration file in /etc/dovecot.
+    ucf --three-way "/usr/share/dovecot/$conffile" "/etc/dovecot/$conffile"
+    ucfr dovecot-imapd "/etc/dovecot/$conffile"
+  done
+
+  cat <<EOF > /usr/share/dovecot/protocols.d/imapd.protocol
+protocols {
+  imap = yes
+}
+EOF
+fi
+
+#DEBHELPER#
diff --git a/dovecot-imapd.postrm b/dovecot-imapd.postrm
new file mode 100644 (file)
index 0000000..dcaaeaf
--- /dev/null
@@ -0,0 +1,29 @@
+#!/bin/sh
+set -e
+
+if [ "$1" = "purge" ] ; then
+       for conffile in $(ucfq --with-colons dovecot-imapd | cut -d: -f1); do
+               # we mimic dpkg as closely as possible, so we remove configuration
+               # files with dpkg backup extensions too:
+               ### Some of the following is from Tore Anderson:
+               for ext in '~' '%' .bak .dpkg-tmp .dpkg-new .dpkg-old .dpkg-dist .ucf-new .ucf-old .ucf-dist;  do
+                       rm -f "$conffile$ext"
+               done
+               # remove the configuration file itself
+               rm -f "$conffile"
+               # and finally clear it out from the ucf database
+               if which ucf >/dev/null; then
+                       ucf --purge "$conffile"
+               fi
+               if which ucfr >/dev/null; then
+                       ucfr --purge dovecot-imapd "$conffile"
+               fi
+       done
+
+fi
+
+if [ "$1" = "purge" ] || [ "$1" = "remove" ]; then
+       rm -f /usr/share/dovecot/protocols.d/imapd.protocol
+fi
+
+#DEBHELPER#
diff --git a/dovecot-imapd.prerm b/dovecot-imapd.prerm
new file mode 100644 (file)
index 0000000..2ad0a1f
--- /dev/null
@@ -0,0 +1,12 @@
+#!/bin/sh
+set -e
+
+if [ -d /run/systemd/system ]; then
+       deb-systemd-invoke stop dovecot.socket || true
+fi
+
+if [ -x "/etc/init.d/dovecot" ]; then
+       invoke-rc.d dovecot stop
+fi
+
+#DEBHELPER#
diff --git a/dovecot-imapd.triggers b/dovecot-imapd.triggers
new file mode 100644 (file)
index 0000000..a71d0af
--- /dev/null
@@ -0,0 +1 @@
+activate-noawait register-dovecot-plugin
diff --git a/dovecot-imapd.ufw.profile b/dovecot-imapd.ufw.profile
new file mode 100644 (file)
index 0000000..17db05c
--- /dev/null
@@ -0,0 +1,11 @@
+[Dovecot IMAP]
+title=Secure mail server (IMAP)
+description=Dovecot is a mail server whose major goals are security and extreme
+ reliability.
+ports=143/tcp
+
+[Dovecot Secure IMAP]
+title=Secure mail server (IMAPS)
+description=Dovecot is a mail server whose major goals are security and extreme
+ reliability.
+ports=993/tcp
diff --git a/dovecot-ldap.links b/dovecot-ldap.links
new file mode 100644 (file)
index 0000000..0f3fcdb
--- /dev/null
@@ -0,0 +1 @@
+/usr/share/bug/dovecot-core /usr/share/bug/dovecot-ldap
diff --git a/dovecot-ldap.postinst b/dovecot-ldap.postinst
new file mode 100644 (file)
index 0000000..2a12fb9
--- /dev/null
@@ -0,0 +1,21 @@
+#!/bin/sh
+
+set -e
+
+if [ "$1" = "configure" ]; then
+  CONFFILES="conf.d/auth-ldap.conf.ext"
+
+  for conffile in $CONFFILES ; do
+    # Tell ucf that the file in /usr/share/dovecot is the latest
+    # maintainer version, and let it handle how to manage the real
+    # configuration file in /etc/dovecot.
+    ucf --three-way "/usr/share/dovecot/$conffile" "/etc/dovecot/$conffile"
+    ucfr dovecot-ldap "/etc/dovecot/$conffile"
+    if [ "$conffile" != "dovecot.conf" ] && [ -f "/etc/dovecot/$conffile" ] &&
+               [ "$(echo "$conffile" | cut -b -7)" != "conf.d/" ]; then
+      chmod 0600 "/etc/dovecot/$conffile"
+    fi
+  done
+fi
+
+#DEBHELPER#
diff --git a/dovecot-ldap.postrm b/dovecot-ldap.postrm
new file mode 100644 (file)
index 0000000..d0405ad
--- /dev/null
@@ -0,0 +1,27 @@
+#!/bin/sh
+set -e
+
+if [ "$1" = "purge" ] ; then
+       CONFFILES="/etc/dovecot/dovecot-ldap.conf.ext \
+               /etc/dovecot/conf.d/auth-ldap.conf.ext"
+
+       for conffile in $CONFFILES; do
+               # we mimic dpkg as closely as possible, so we remove configuration
+               # files with dpkg backup extensions too:
+               ### Some of the following is from Tore Anderson:
+               for ext in '~' '%' .bak .dpkg-tmp .dpkg-new .dpkg-old .dpkg-dist .ucf-new .ucf-old .ucf-dist;  do
+                       rm -f "$conffile$ext"
+               done
+               # remove the configuration file itself
+               rm -f "$conffile"
+               # and finally clear it out from the ucf database
+               if which ucf >/dev/null; then
+                       ucf --purge "$conffile"
+               fi
+               if which ucfr >/dev/null; then
+                       ucfr --purge dovecot-ldap "$conffile"
+               fi
+       done
+fi
+
+#DEBHELPER#
diff --git a/dovecot-ldap.triggers b/dovecot-ldap.triggers
new file mode 100644 (file)
index 0000000..903b9e7
--- /dev/null
@@ -0,0 +1,2 @@
+activate-noawait register-dovecot-plugin
+
diff --git a/dovecot-lmtpd.links b/dovecot-lmtpd.links
new file mode 100644 (file)
index 0000000..bc95bf0
--- /dev/null
@@ -0,0 +1 @@
+/usr/share/bug/dovecot-core /usr/share/bug/dovecot-lmtpd
diff --git a/dovecot-lmtpd.postinst b/dovecot-lmtpd.postinst
new file mode 100644 (file)
index 0000000..de561e1
--- /dev/null
@@ -0,0 +1,23 @@
+#!/bin/sh
+
+set -e
+
+if [ "$1" = "configure" ]; then
+  CONFFILES="conf.d/20-lmtp.conf"
+
+  for conffile in $CONFFILES ; do
+    # Tell ucf that the file in /usr/share/dovecot is the latest
+    # maintainer version, and let it handle how to manage the real
+    # configuration file in /etc/dovecot.
+    ucf --three-way "/usr/share/dovecot/$conffile" "/etc/dovecot/$conffile"
+    ucfr dovecot-lmtpd "/etc/dovecot/$conffile"
+  done
+
+  cat <<EOF > /usr/share/dovecot/protocols.d/lmtpd.protocol
+protocols {
+  lmtp = yes
+}
+EOF
+fi
+
+#DEBHELPER#
diff --git a/dovecot-lmtpd.postrm b/dovecot-lmtpd.postrm
new file mode 100644 (file)
index 0000000..f0b9920
--- /dev/null
@@ -0,0 +1,29 @@
+#!/bin/sh
+set -e
+
+if [ "$1" = "purge" ] ; then
+       for conffile in $(ucfq --with-colons dovecot-lmtpd | cut -d: -f1); do
+               # we mimic dpkg as closely as possible, so we remove configuration
+               # files with dpkg backup extensions too:
+               ### Some of the following is from Tore Anderson:
+               for ext in '~' '%' .bak .dpkg-tmp .dpkg-new .dpkg-old .dpkg-dist .ucf-new .ucf-old .ucf-dist;  do
+                       rm -f "$conffile$ext"
+               done
+               # remove the configuration file itself
+               rm -f "$conffile"
+               # and finally clear it out from the ucf database
+               if which ucf >/dev/null; then
+                       ucf --purge "$conffile"
+               fi
+               if which ucfr >/dev/null; then
+                       ucfr --purge dovecot-lmtpd "$conffile"
+               fi
+       done
+
+fi
+
+if [ "$1" = "purge" ] || [ "$1" = "remove" ]; then
+       rm -f /usr/share/dovecot/protocols.d/lmtpd.protocol
+fi
+
+#DEBHELPER#
diff --git a/dovecot-lmtpd.prerm b/dovecot-lmtpd.prerm
new file mode 100644 (file)
index 0000000..2ad0a1f
--- /dev/null
@@ -0,0 +1,12 @@
+#!/bin/sh
+set -e
+
+if [ -d /run/systemd/system ]; then
+       deb-systemd-invoke stop dovecot.socket || true
+fi
+
+if [ -x "/etc/init.d/dovecot" ]; then
+       invoke-rc.d dovecot stop
+fi
+
+#DEBHELPER#
diff --git a/dovecot-lmtpd.triggers b/dovecot-lmtpd.triggers
new file mode 100644 (file)
index 0000000..903b9e7
--- /dev/null
@@ -0,0 +1,2 @@
+activate-noawait register-dovecot-plugin
+
diff --git a/dovecot-managesieved.README.Debian b/dovecot-managesieved.README.Debian
new file mode 100644 (file)
index 0000000..6c6aa4a
--- /dev/null
@@ -0,0 +1,17 @@
+Removing dovecot-managesieved
+-----------------------------
+
+If you remove (i.e. with dpkg -r or aptitude remove) this package you will
+get an error something like this:
+
+doveconf: Fatal: Error in configuration file /etc/dovecot/dovecot.conf: 
+service(managesieve-login): executable is empty
+
+To avoid it, make sure 
+/etc/dovecot/conf.d/etc/dovecot/conf.d/20-managesieve.conf has been deleted, 
+moved or completely commented out first.  Alternatively, purge the package 
+instead of removing it.
+
+If you do not do this, dovecot-core will be left in an unconfigured state 
+because the error will cause a trigger to fail.  To fix this simply reinstall 
+dovecot-core.
diff --git a/dovecot-managesieved.install b/dovecot-managesieved.install
new file mode 100644 (file)
index 0000000..14e7229
--- /dev/null
@@ -0,0 +1 @@
+pigeonhole/doc/example-config/conf.d/??-managesieve*   usr/share/dovecot/conf.d
diff --git a/dovecot-managesieved.links b/dovecot-managesieved.links
new file mode 100644 (file)
index 0000000..9f3ea79
--- /dev/null
@@ -0,0 +1 @@
+/usr/share/bug/dovecot-core /usr/share/bug/dovecot-managesieved
diff --git a/dovecot-managesieved.lintian-overrides b/dovecot-managesieved.lintian-overrides
new file mode 100644 (file)
index 0000000..4d9c9a1
--- /dev/null
@@ -0,0 +1,2 @@
+dovecot-managesieved: shared-library-lacks-prerequisites [usr/lib/dovecot/modules/settings/libmanagesieve_settings.so]
+dovecot-managesieved: library-not-linked-against-libc [usr/lib/dovecot/modules/settings/libmanagesieve_settings.so]
diff --git a/dovecot-managesieved.postinst b/dovecot-managesieved.postinst
new file mode 100644 (file)
index 0000000..4544e1e
--- /dev/null
@@ -0,0 +1,23 @@
+#!/bin/sh
+
+set -e
+
+if [ "$1" = "configure" ]; then
+  CONFFILES="conf.d/20-managesieve.conf"
+
+  for conffile in $CONFFILES ; do
+    # Tell ucf that the file in /usr/share/dovecot is the latest
+    # maintainer version, and let it handle how to manage the real
+    # configuration file in /etc/dovecot.
+    ucf --three-way "/usr/share/dovecot/$conffile" "/etc/dovecot/$conffile"
+    ucfr dovecot-managesieved "/etc/dovecot/$conffile"
+  done
+
+  cat <<EOF > /usr/share/dovecot/protocols.d/managesieved.protocol
+protocols {
+  sieve = yes
+}
+EOF
+fi
+
+#DEBHELPER#
diff --git a/dovecot-managesieved.postrm b/dovecot-managesieved.postrm
new file mode 100644 (file)
index 0000000..f464ccf
--- /dev/null
@@ -0,0 +1,29 @@
+#!/bin/sh
+set -e
+
+if [ "$1" = "purge" ] ; then
+       for conffile in $(ucfq --with-colons dovecot-managesieved | cut -d: -f1); do
+               # we mimic dpkg as closely as possible, so we remove configuration
+               # files with dpkg backup extensions too:
+               ### Some of the following is from Tore Anderson:
+               for ext in '~' '%' .bak .dpkg-tmp .dpkg-new .dpkg-old .dpkg-dist .ucf-new .ucf-old .ucf-dist;  do
+                       rm -f "$conffile$ext"
+               done
+               # remove the configuration file itself
+               rm -f "$conffile"
+               # and finally clear it out from the ucf database
+               if which ucf >/dev/null; then
+                       ucf --purge "$conffile"
+               fi
+               if which ucfr >/dev/null; then
+                       ucfr --purge dovecot-managesieved "$conffile"
+               fi
+       done
+
+fi
+
+if [ "$1" = "purge" ] || [ "$1" = "remove" ]; then
+       rm -f /usr/share/dovecot/protocols.d/managesieved.protocol
+fi
+
+#DEBHELPER#
diff --git a/dovecot-managesieved.prerm b/dovecot-managesieved.prerm
new file mode 100644 (file)
index 0000000..2ad0a1f
--- /dev/null
@@ -0,0 +1,12 @@
+#!/bin/sh
+set -e
+
+if [ -d /run/systemd/system ]; then
+       deb-systemd-invoke stop dovecot.socket || true
+fi
+
+if [ -x "/etc/init.d/dovecot" ]; then
+       invoke-rc.d dovecot stop
+fi
+
+#DEBHELPER#
diff --git a/dovecot-managesieved.triggers b/dovecot-managesieved.triggers
new file mode 100644 (file)
index 0000000..903b9e7
--- /dev/null
@@ -0,0 +1,2 @@
+activate-noawait register-dovecot-plugin
+
diff --git a/dovecot-mysql.links b/dovecot-mysql.links
new file mode 100644 (file)
index 0000000..321547d
--- /dev/null
@@ -0,0 +1 @@
+/usr/share/bug/dovecot-core /usr/share/bug/dovecot-mysql
diff --git a/dovecot-mysql.triggers b/dovecot-mysql.triggers
new file mode 100644 (file)
index 0000000..903b9e7
--- /dev/null
@@ -0,0 +1,2 @@
+activate-noawait register-dovecot-plugin
+
diff --git a/dovecot-openssl.cnf b/dovecot-openssl.cnf
new file mode 100644 (file)
index 0000000..90bf50c
--- /dev/null
@@ -0,0 +1,23 @@
+#
+# SSLeay configuration file for Dovecot.
+#
+
+RANDFILE                = /dev/urandom
+
+[ req ]
+default_bits            = 2048
+default_keyfile         = privkey.pem
+distinguished_name      = req_distinguished_name
+prompt                  = no
+policy                 = policy_anything
+req_extensions          = v3_req
+x509_extensions         = v3_req
+
+[ req_distinguished_name ]
+organizationName = Dovecot mail server
+organizationalUnitName = @commonName@
+commonName = @commonName@
+emailAddress = @emailAddress@
+
+[ v3_req ]
+basicConstraints        = CA:FALSE
diff --git a/dovecot-pgsql.links b/dovecot-pgsql.links
new file mode 100644 (file)
index 0000000..9b4831f
--- /dev/null
@@ -0,0 +1 @@
+/usr/share/bug/dovecot-core /usr/share/bug/dovecot-pgsql
diff --git a/dovecot-pgsql.triggers b/dovecot-pgsql.triggers
new file mode 100644 (file)
index 0000000..903b9e7
--- /dev/null
@@ -0,0 +1,2 @@
+activate-noawait register-dovecot-plugin
+
diff --git a/dovecot-pop3d.links b/dovecot-pop3d.links
new file mode 100644 (file)
index 0000000..24ce7f6
--- /dev/null
@@ -0,0 +1 @@
+/usr/share/bug/dovecot-core /usr/share/bug/dovecot-pop3d
diff --git a/dovecot-pop3d.postinst b/dovecot-pop3d.postinst
new file mode 100644 (file)
index 0000000..8560956
--- /dev/null
@@ -0,0 +1,23 @@
+#!/bin/sh
+
+set -e
+
+if [ "$1" = "configure" ]; then
+  CONFFILES="conf.d/20-pop3.conf"
+
+  for conffile in $CONFFILES ; do
+    # Tell ucf that the file in /usr/share/dovecot is the latest
+    # maintainer version, and let it handle how to manage the real
+    # configuration file in /etc/dovecot.
+    ucf --three-way "/usr/share/dovecot/$conffile" "/etc/dovecot/$conffile"
+    ucfr dovecot-pop3d "/etc/dovecot/$conffile"
+  done
+
+  cat <<EOF > /usr/share/dovecot/protocols.d/pop3d.protocol
+protocols {
+  pop3 = yes
+}
+EOF
+fi
+
+#DEBHELPER#
diff --git a/dovecot-pop3d.postrm b/dovecot-pop3d.postrm
new file mode 100644 (file)
index 0000000..0c858f1
--- /dev/null
@@ -0,0 +1,29 @@
+#!/bin/sh
+set -e
+
+if [ "$1" = "purge" ] ; then
+       for conffile in $(ucfq --with-colons dovecot-pop3d | cut -d: -f1); do
+               # we mimic dpkg as closely as possible, so we remove configuration
+               # files with dpkg backup extensions too:
+               ### Some of the following is from Tore Anderson:
+               for ext in '~' '%' .bak .dpkg-tmp .dpkg-new .dpkg-old .dpkg-dist .ucf-new .ucf-old .ucf-dist;  do
+                       rm -f "$conffile$ext"
+               done
+               # remove the configuration file itself
+               rm -f "$conffile"
+               # and finally clear it out from the ucf database
+               if which ucf >/dev/null; then
+                       ucf --purge "$conffile"
+               fi
+               if which ucfr >/dev/null; then
+                       ucfr --purge dovecot-pop3d "$conffile"
+               fi
+       done
+
+fi
+
+if [ "$1" = "purge" ] || [ "$1" = "remove" ]; then
+       rm -f /usr/share/dovecot/protocols.d/pop3d.protocol
+fi
+
+#DEBHELPER#
diff --git a/dovecot-pop3d.prerm b/dovecot-pop3d.prerm
new file mode 100644 (file)
index 0000000..2ad0a1f
--- /dev/null
@@ -0,0 +1,12 @@
+#!/bin/sh
+set -e
+
+if [ -d /run/systemd/system ]; then
+       deb-systemd-invoke stop dovecot.socket || true
+fi
+
+if [ -x "/etc/init.d/dovecot" ]; then
+       invoke-rc.d dovecot stop
+fi
+
+#DEBHELPER#
diff --git a/dovecot-pop3d.triggers b/dovecot-pop3d.triggers
new file mode 100644 (file)
index 0000000..903b9e7
--- /dev/null
@@ -0,0 +1,2 @@
+activate-noawait register-dovecot-plugin
+
diff --git a/dovecot-pop3d.ufw.profile b/dovecot-pop3d.ufw.profile
new file mode 100644 (file)
index 0000000..a46df40
--- /dev/null
@@ -0,0 +1,11 @@
+[Dovecot POP3]
+title=Secure mail server (POP3)
+description=Dovecot is a mail server whose major goals are security and extreme
+ reliability.
+ports=110/tcp
+
+[Dovecot Secure POP3]
+title=Secure mail server (POP3S)
+description=Dovecot is a mail server whose major goals are security and extreme
+ reliability.
+ports=995/tcp
diff --git a/dovecot-sieve.links b/dovecot-sieve.links
new file mode 100644 (file)
index 0000000..971b50a
--- /dev/null
@@ -0,0 +1 @@
+/usr/share/bug/dovecot-core /usr/share/bug/dovecot-sieve
diff --git a/dovecot-sieve.lintian-overrides b/dovecot-sieve.lintian-overrides
new file mode 100644 (file)
index 0000000..840cfef
--- /dev/null
@@ -0,0 +1,4 @@
+# ignore internal library without dependency information
+dovecot-sieve: shared-library-lacks-prerequisites [usr/lib/dovecot/modules/sieve/lib90_sieve_imapsieve_plugin.so]
+# alias for sieve-dump
+dovecot-sieve: spare-manual-page [usr/share/man/man1/sieved.1.gz]
diff --git a/dovecot-sieve.postinst b/dovecot-sieve.postinst
new file mode 100644 (file)
index 0000000..6fb5244
--- /dev/null
@@ -0,0 +1,18 @@
+#!/bin/sh
+
+set -e
+
+if [ "$1" = "configure" ]; then
+  CONFFILES="conf.d/90-sieve.conf \
+  conf.d/90-sieve-extprograms.conf"
+
+  for conffile in $CONFFILES ; do
+    # Tell ucf that the file in /usr/share/dovecot is the latest
+    # maintainer version, and let it handle how to manage the real
+    # configuration file in /etc/dovecot.
+    ucf --three-way "/usr/share/dovecot/$conffile" "/etc/dovecot/$conffile"
+    ucfr dovecot-sieve "/etc/dovecot/$conffile"
+  done
+fi
+
+#DEBHELPER#
diff --git a/dovecot-sieve.postrm b/dovecot-sieve.postrm
new file mode 100644 (file)
index 0000000..db48cfc
--- /dev/null
@@ -0,0 +1,24 @@
+#!/bin/sh
+set -e
+
+if [ "$1" = "purge" ] ; then
+       for conffile in $(ucfq --with-colons dovecot-sieve | cut -d: -f1); do
+               # we mimic dpkg as closely as possible, so we remove configuration
+               # files with dpkg backup extensions too:
+               ### Some of the following is from Tore Anderson:
+               for ext in '~' '%' .bak .dpkg-tmp .dpkg-new .dpkg-old .dpkg-dist .ucf-new .ucf-old .ucf-dist;  do
+                       rm -f "$conffile$ext"
+               done
+               # remove the configuration file itself
+               rm -f "$conffile"
+               # and finally clear it out from the ucf database
+               if which ucf >/dev/null; then
+                       ucf --purge "$conffile"
+               fi
+               if which ucfr >/dev/null; then
+                       ucfr --purge dovecot-sieve "$conffile"
+               fi
+       done
+fi
+
+#DEBHELPER#
diff --git a/dovecot-sieve.triggers b/dovecot-sieve.triggers
new file mode 100644 (file)
index 0000000..903b9e7
--- /dev/null
@@ -0,0 +1,2 @@
+activate-noawait register-dovecot-plugin
+
diff --git a/dovecot-solr.README.Debian b/dovecot-solr.README.Debian
new file mode 100644 (file)
index 0000000..edc5d75
--- /dev/null
@@ -0,0 +1,2 @@
+After installing this package, the solr XML schema can be found in 
+/usr/share/dovecot/solr-schema-*.xml
diff --git a/dovecot-solr.install b/dovecot-solr.install
new file mode 100644 (file)
index 0000000..c14caf4
--- /dev/null
@@ -0,0 +1 @@
+doc/solr-*.xml usr/share/dovecot
diff --git a/dovecot-solr.links b/dovecot-solr.links
new file mode 100644 (file)
index 0000000..7536822
--- /dev/null
@@ -0,0 +1 @@
+/usr/share/bug/dovecot-core /usr/share/bug/dovecot-solr
diff --git a/dovecot-solr.triggers b/dovecot-solr.triggers
new file mode 100644 (file)
index 0000000..903b9e7
--- /dev/null
@@ -0,0 +1,2 @@
+activate-noawait register-dovecot-plugin
+
diff --git a/dovecot-sqlite.links b/dovecot-sqlite.links
new file mode 100644 (file)
index 0000000..2204ea7
--- /dev/null
@@ -0,0 +1 @@
+/usr/share/bug/dovecot-core /usr/share/bug/dovecot-sqlite
diff --git a/dovecot-sqlite.triggers b/dovecot-sqlite.triggers
new file mode 100644 (file)
index 0000000..903b9e7
--- /dev/null
@@ -0,0 +1,2 @@
+activate-noawait register-dovecot-plugin
+
diff --git a/dovecot-submissiond.postinst b/dovecot-submissiond.postinst
new file mode 100644 (file)
index 0000000..7459606
--- /dev/null
@@ -0,0 +1,27 @@
+#!/bin/sh
+
+set -e
+
+if [ "$1" = "configure" ]; then
+  CONFFILES="conf.d/20-submission.conf"
+
+  for conffile in $CONFFILES ; do
+    # Tell ucf that the file in /usr/share/dovecot is the latest
+    # maintainer version, and let it handle how to manage the real
+    # configuration file in /etc/dovecot.
+    ucf --three-way "/usr/share/dovecot/$conffile" "/etc/dovecot/$conffile"
+    # Use --force to allow hijacking the file from dovecot-submission
+    ucfr --force dovecot-submissiond "/etc/dovecot/$conffile"
+  done
+
+  if [ -f /usr/share/dovecot/protocols.d/submission.protocol ]; then
+    rm -f /usr/share/dovecot/protocols.d/submission.protocol
+  fi
+  cat <<EOF > /usr/share/dovecot/protocols.d/submissiond.protocol
+protocols {
+  submission = yes
+}
+EOF
+fi
+
+#DEBHELPER#
diff --git a/dovecot-submissiond.postrm b/dovecot-submissiond.postrm
new file mode 100644 (file)
index 0000000..eaea028
--- /dev/null
@@ -0,0 +1,29 @@
+#!/bin/sh
+set -e
+
+if [ "$1" = "purge" ] ; then
+       for conffile in $(ucfq --with-colons dovecot-submissiond | cut -d: -f1); do
+               # we mimic dpkg as closely as possible, so we remove configuration
+               # files with dpkg backup extensions too:
+               ### Some of the following is from Tore Anderson:
+               for ext in '~' '%' .bak .dpkg-tmp .dpkg-new .dpkg-old .dpkg-dist .ucf-new .ucf-old .ucf-dist;  do
+                       rm -f "$conffile$ext"
+               done
+               # remove the configuration file itself
+               rm -f "$conffile"
+               # and finally clear it out from the ucf database
+               if which ucf >/dev/null; then
+                       ucf --purge "$conffile"
+               fi
+               if which ucfr >/dev/null; then
+                       ucfr --purge dovecot-submissiond "$conffile"
+               fi
+       done
+
+fi
+
+if [ "$1" = "purge" ] || [ "$1" = "remove" ]; then
+       rm -f /usr/share/dovecot/protocols.d/submissiond.protocol
+fi
+
+#DEBHELPER#
diff --git a/dovecot-submissiond.prerm b/dovecot-submissiond.prerm
new file mode 100644 (file)
index 0000000..2ad0a1f
--- /dev/null
@@ -0,0 +1,12 @@
+#!/bin/sh
+set -e
+
+if [ -d /run/systemd/system ]; then
+       deb-systemd-invoke stop dovecot.socket || true
+fi
+
+if [ -x "/etc/init.d/dovecot" ]; then
+       invoke-rc.d dovecot stop
+fi
+
+#DEBHELPER#
diff --git a/dovecot-submissiond.triggers b/dovecot-submissiond.triggers
new file mode 100644 (file)
index 0000000..a71d0af
--- /dev/null
@@ -0,0 +1 @@
+activate-noawait register-dovecot-plugin
diff --git a/gbp.conf b/gbp.conf
new file mode 100644 (file)
index 0000000..d2026bc
--- /dev/null
+++ b/gbp.conf
@@ -0,0 +1,14 @@
+[DEFAULT]
+pristine-tar = True
+sign-tags = True
+component=pigeonhole
+
+[buildpackage]
+export-dir = ../build-area/
+
+[dch]
+meta = True
+id-length = 7
+
+[pq]
+patch-numbers = False
diff --git a/maildirmake.dovecot b/maildirmake.dovecot
new file mode 100644 (file)
index 0000000..5313d13
--- /dev/null
@@ -0,0 +1,28 @@
+#!/bin/sh
+#
+# maildirmake.dovecot -- create maildirs
+# Copyright (c) 2003, Jaldhar H. Vyas
+# "Do what thou wilt" shall be the whole of the license.
+#
+dir="$1"
+owner="$2"
+if [ -z "$dir" ]; then
+  echo "Must supply a directory path"
+  exit 1
+fi 
+
+if [ "$dir" = "-h" ]; then
+  echo "usage: $0 directory [user]"
+  exit 0
+fi 
+
+umask 077
+mkdir -p "$dir/cur" "$dir/new" "$dir/tmp" || exit 1
+chmod 0700 "$dir" "$dir/cur" "$dir/new" "$dir/tmp" || exit 1
+
+if [ -n "$owner" ]; then 
+  chown -R "$owner" "$dir" || exit 1
+fi
+
+exit 0
+
diff --git a/maildirmake.dovecot.1 b/maildirmake.dovecot.1
new file mode 100644 (file)
index 0000000..a814c81
--- /dev/null
@@ -0,0 +1,46 @@
+.\"                                      Hey, EMACS: -*- nroff -*-
+.\" First parameter, NAME, should be all caps
+.\" Second parameter, SECTION, should be 1-8, maybe w/ subsection
+.\" other parameters are allowed: see man(7), man(1)
+.TH "MAILDIRMAKE.DOVECOT" "1" "23 November 2005"
+.\" Please adjust this date whenever revising the manpage.
+.\"
+.\" Some roff macros, for reference:
+.\" .nh        disable hyphenation
+.\" .hy        enable hyphenation
+.\" .ad l      left justify
+.\" .ad b      justify to both left and right margins
+.\" .nf        disable filling
+.\" .fi        enable filling
+.\" .br        insert line break
+.\" .sp <n>    insert n+1 empty lines
+.\" for manpage-specific macros, see man(7)
+.SH NAME
+maildirmake.dovecot \- creates maildirs and maildir subfolders
+.SH SYNOPSIS
+.B maildirmake.dovecot
+\fBdirectory\fR [ \fRowner\fR ] \fR
+.br
+.SH DESCRIPTION
+.\" TeX users may be more comfortable with the \fB<whatever>\fP and
+.\" \fI<whatever>\fP escape sequences to invode bold face and italics, 
+.\" respectively.
+\fBmaildirmake.dovecot\fP creates a maildir and its maildir subfolders (cur, new, tmp).
+\fBdirectory\fP is the name of the new maildir. If \fBdirectory\fP exists cur, new, tmp directories are created inside it.
+.TP
+You can specify \fBowner\fP to change directories ownership if you have ownership modification permission.
+.TP
+\fBmaildirmake.dovecot\fP is very basic. It is strongly recommended to use \fBmaildirmake\fP if you want to do serious maildir management.
+.SH OPTIONS
+.TP
+\fB-h\fP
+Show options summary and exits immediately.
+.SH SEE ALSO
+.BR maildirmake (1).
+.BR maildir (5).
+.SH AUTHOR
+.TP
+dovecot was written by Timo Sirainen <tss@iki.fi>. maildirmake.dovecot script was written by Jaldhar H. Vyas
+.PP
+This manual page was written by Henry Precheur <henry@precheur.org>,
+for the Debian project (but may be used by others).
diff --git a/patches/0001-auth-db-oauth2-Reduce-nesting-in-token_in_scope-via-.patch b/patches/0001-auth-db-oauth2-Reduce-nesting-in-token_in_scope-via-.patch
new file mode 100644 (file)
index 0000000..557c2fe
--- /dev/null
@@ -0,0 +1,71 @@
+From 4c8919f9470ee61efe318fa4f7d410ba12098eed Mon Sep 17 00:00:00 2001
+From: Aki Tuomi <aki.tuomi@open-xchange.com>
+Date: Wed, 3 Jun 2026 12:56:13 +0000
+Subject: [PATCH 1/4] auth: db-oauth2: Reduce nesting in token_in_scope via
+ early return
+
+---
+ src/auth/db-oauth2.c | 48 ++++++++++++++++++++++----------------------
+ 1 file changed, 24 insertions(+), 24 deletions(-)
+
+Index: trixie/src/auth/db-oauth2.c
+===================================================================
+--- trixie.orig/src/auth/db-oauth2.c
++++ trixie/src/auth/db-oauth2.c
+@@ -553,32 +553,32 @@ static bool
+ db_oauth2_token_in_scope(struct db_oauth2_request *req,
+                        enum passdb_result *result_r, const char **error_r)
+ {
+-      bool found = TRUE;
+-      if (!array_is_empty(&req->db->set->scope)) {
+-              found = FALSE;
+-              const char *value = auth_fields_find(req->fields, "scope");
+-              bool has_scope = value != NULL;
+-              if (!has_scope)
+-                      value = auth_fields_find(req->fields, "aud");
+-              e_debug(authdb_event(req->auth_request),
+-                      "Token scope(s): %s",
+-                      value);
+-              if (value != NULL) {
+-                      const char *wanted_scope;
+-                      const char *const *entries = has_scope ?
+-                              t_strsplit_spaces(value, " ") :
+-                              t_strsplit_tabescaped(value);
+-                      array_foreach_elem(&req->db->set->scope, wanted_scope) {
+-                              if ((found = str_array_find(entries, wanted_scope)))
+-                                      break;
+-                      }
+-              }
+-              if (!found) {
+-                      *error_r = t_strdup_printf("Token is not valid for scope '%s'",
+-                                                 req->db->oauth2_set.scope);
+-                      *result_r = PASSDB_RESULT_USER_DISABLED;
++      if (array_is_empty(&req->db->set->scope))
++              return TRUE;
++
++      bool found = FALSE;
++      const char *value = auth_fields_find(req->fields, "scope");
++      bool has_scope = value != NULL;
++      if (!has_scope)
++              value = auth_fields_find(req->fields, "aud");
++      e_debug(authdb_event(req->auth_request),
++              "Token scope(s): %s",
++              value);
++      if (value != NULL) {
++              const char *wanted_scope;
++              const char *const *entries = has_scope ?
++                      t_strsplit_spaces(value, " ") :
++                      t_strsplit_tabescaped(value);
++              array_foreach_elem(&req->db->set->scope, wanted_scope) {
++                      if ((found = str_array_find(entries, wanted_scope)))
++                              break;
+               }
+       }
++      if (!found) {
++              *error_r = t_strdup_printf("Token is not valid for scope '%s'",
++                      t_array_const_string_join(&req->db->set->scope, " "));
++              *result_r = PASSDB_RESULT_USER_DISABLED;
++      }
+       return found;
+ }
+ 
diff --git a/patches/0001-commin-Rename-LOGIN_PROXY_FAILURE_TYPE_AUTH-to-LOGIN.patch b/patches/0001-commin-Rename-LOGIN_PROXY_FAILURE_TYPE_AUTH-to-LOGIN.patch
new file mode 100644 (file)
index 0000000..05a13f5
--- /dev/null
@@ -0,0 +1,143 @@
+From d1b2e9e028b5bb8501b054c13c5208fbb4eff21f Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Fri, 18 Jul 2025 14:26:21 +0300
+Subject: [PATCH] *-commin: Rename LOGIN_PROXY_FAILURE_TYPE_AUTH to
+ LOGIN_PROXY_FAILURE_TYPE_AUTH_REPLIED
+
+---
+ src/imap-login/imap-proxy.c             | 4 ++--
+ src/login-common/client-common-auth.c   | 2 +-
+ src/login-common/client-common.c        | 2 +-
+ src/login-common/login-proxy.c          | 4 ++--
+ src/login-common/login-proxy.h          | 2 +-
+ src/pop3-login/pop3-proxy.c             | 4 ++--
+ src/submission-login/submission-proxy.c | 4 ++--
+ 7 files changed, 11 insertions(+), 11 deletions(-)
+
+Index: trixie/src/imap-login/imap-proxy.c
+===================================================================
+--- trixie.orig/src/imap-login/imap-proxy.c
++++ trixie/src/imap-login/imap-proxy.c
+@@ -427,7 +427,7 @@ int imap_proxy_parse_line(struct client
+               const char *log_line = line;
+               (void)str_begins_icase(log_line, "NO ", &log_line);
+               enum login_proxy_failure_type failure_type =
+-                      LOGIN_PROXY_FAILURE_TYPE_AUTH;
++                      LOGIN_PROXY_FAILURE_TYPE_AUTH_REPLIED;
+ #define STR_NO_IMAP_RESP_CODE_AUTHFAILED "NO ["IMAP_RESP_CODE_AUTHFAILED"]"
+               if (str_begins_with(line, STR_NO_IMAP_RESP_CODE_AUTHFAILED)) {
+                       /* the remote sent a generic "authentication failed"
+@@ -586,7 +586,7 @@ imap_proxy_send_failure_reply(struct ima
+               client_send_raw(&imap_client->common, t_strconcat(
+                       imap_client->cmd_tag, " NO ", reason, "\r\n", NULL));
+               break;
+-      case LOGIN_PROXY_FAILURE_TYPE_AUTH:
++      case LOGIN_PROXY_FAILURE_TYPE_AUTH_REPLIED:
+               /* reply was already sent */
+               break;
+       }
+Index: trixie/src/login-common/client-common-auth.c
+===================================================================
+--- trixie.orig/src/login-common/client-common-auth.c
++++ trixie/src/login-common/client-common-auth.c
+@@ -426,7 +426,7 @@ proxy_redirect_reauth_callback(struct au
+                       /* Disconnect from the original backend */
+                       login_proxy_failed(client->login_proxy,
+                               login_proxy_get_event(client->login_proxy),
+-                              LOGIN_PROXY_FAILURE_TYPE_AUTH,
++                              LOGIN_PROXY_FAILURE_TYPE_AUTH_REPLIED,
+                               t_strdup_printf("Redirected to %s", reply.proxy.host));
+                       return;
+               }
+Index: trixie/src/login-common/client-common.c
+===================================================================
+--- trixie.orig/src/login-common/client-common.c
++++ trixie/src/login-common/client-common.c
+@@ -1385,7 +1385,7 @@ bool client_get_extra_disconnect_reason(
+                       event_reason = "protocol_failure";
+                       last_reason = "protocol failure";
+                       break;
+-              case LOGIN_PROXY_FAILURE_TYPE_AUTH:
++              case LOGIN_PROXY_FAILURE_TYPE_AUTH_REPLIED:
+                       event_reason = "auth_failed";
+                       last_reason = "authentication failure";
+                       break;
+Index: trixie/src/login-common/login-proxy.c
+===================================================================
+--- trixie.orig/src/login-common/login-proxy.c
++++ trixie/src/login-common/login-proxy.c
+@@ -829,7 +829,7 @@ bool login_proxy_failed(struct login_pro
+       case LOGIN_PROXY_FAILURE_TYPE_PROTOCOL:
+               log_prefix = "Remote server sent invalid input: ";
+               break;
+-      case LOGIN_PROXY_FAILURE_TYPE_AUTH:
++      case LOGIN_PROXY_FAILURE_TYPE_AUTH_REPLIED:
+               log_prefix = "";
+               try_reconnect = FALSE;
+               break;
+@@ -854,7 +854,7 @@ bool login_proxy_failed(struct login_pro
+               return TRUE;
+       }
+ 
+-      if (type != LOGIN_PROXY_FAILURE_TYPE_AUTH &&
++      if (type != LOGIN_PROXY_FAILURE_TYPE_AUTH_REPLIED &&
+           type != LOGIN_PROXY_FAILURE_TYPE_AUTH_TEMPFAIL)
+               e_error(event, "%s%s", log_prefix, reason);
+       else if (proxy->client->set->auth_verbose)
+Index: trixie/src/login-common/login-proxy.h
+===================================================================
+--- trixie.orig/src/login-common/login-proxy.h
++++ trixie/src/login-common/login-proxy.h
+@@ -34,7 +34,7 @@ enum login_proxy_failure_type {
+       LOGIN_PROXY_FAILURE_TYPE_PROTOCOL,
+       /* Authentication failed to backend. The LOGIN/AUTH command reply was
+          already sent to the client. */
+-      LOGIN_PROXY_FAILURE_TYPE_AUTH,
++      LOGIN_PROXY_FAILURE_TYPE_AUTH_REPLIED,
+       /* Authentication failed with a temporary failure code. Attempting it
+          again might work. */
+       LOGIN_PROXY_FAILURE_TYPE_AUTH_TEMPFAIL,
+Index: trixie/src/pop3-login/pop3-proxy.c
+===================================================================
+--- trixie.orig/src/pop3-login/pop3-proxy.c
++++ trixie/src/pop3-login/pop3-proxy.c
+@@ -322,7 +322,7 @@ int pop3_proxy_parse_line(struct client
+          shouldn't be a real problem since of course everyone will
+          be using only Dovecot as their backend :) */
+       enum login_proxy_failure_type failure_type =
+-              LOGIN_PROXY_FAILURE_TYPE_AUTH;
++              LOGIN_PROXY_FAILURE_TYPE_AUTH_REPLIED;
+       if (!str_begins_with(line, "-ERR ")) {
+               client_send_reply(client, POP3_CMD_REPLY_ERROR,
+                                 AUTH_FAILED_MSG);
+@@ -373,7 +373,7 @@ pop3_proxy_send_failure_reply(struct cli
+               /* [SYS/TEMP] prefix is already in the reason string */
+               client_send_reply(client, POP3_CMD_REPLY_ERROR, reason);
+               break;
+-      case LOGIN_PROXY_FAILURE_TYPE_AUTH:
++      case LOGIN_PROXY_FAILURE_TYPE_AUTH_REPLIED:
+               /* reply was already sent */
+               break;
+       }
+Index: trixie/src/submission-login/submission-proxy.c
+===================================================================
+--- trixie.orig/src/submission-login/submission-proxy.c
++++ trixie/src/submission-login/submission-proxy.c
+@@ -693,7 +693,7 @@ int submission_proxy_parse_line(struct c
+          shouldn't be a real problem since of course everyone will
+          be using only Dovecot as their backend :) */
+       enum login_proxy_failure_type failure_type =
+-              LOGIN_PROXY_FAILURE_TYPE_AUTH;
++              LOGIN_PROXY_FAILURE_TYPE_AUTH_REPLIED;
+       if ((status / 100) == 4)
+               failure_type = LOGIN_PROXY_FAILURE_TYPE_AUTH_TEMPFAIL;
+       else if (!submission_proxy_handle_redirect(
+@@ -749,7 +749,7 @@ submission_proxy_send_failure_reply(stru
+               i_assert(subm_client->proxy_reply != NULL);
+               smtp_server_reply_submit(subm_client->proxy_reply);
+               break;
+-      case LOGIN_PROXY_FAILURE_TYPE_AUTH:
++      case LOGIN_PROXY_FAILURE_TYPE_AUTH_REPLIED:
+               /* reply was already sent */
+               i_assert(cmd == NULL);
+               break;
diff --git a/patches/0001-dsync-Avoid-potentially-excessive-data-stack-growth-.patch b/patches/0001-dsync-Avoid-potentially-excessive-data-stack-growth-.patch
new file mode 100644 (file)
index 0000000..87c406e
--- /dev/null
@@ -0,0 +1,40 @@
+From b40043ec2e45b79e9cb185009cffadfb1448da7e Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Tue, 24 Mar 2026 12:02:00 +0200
+Subject: [PATCH 1/2] dsync: Avoid potentially excessive data stack growth for
+ mailbox attribute sending
+
+The attribute value can be large.
+---
+ src/doveadm/dsync/dsync-ibc-stream.c | 3 ++-
+ 1 file changed, 2 insertions(+), 1 deletion(-)
+
+diff --git a/src/doveadm/dsync/dsync-ibc-stream.c b/src/doveadm/dsync/dsync-ibc-stream.c
+index 472d1fc517..0b3038201d 100644
+--- a/src/doveadm/dsync/dsync-ibc-stream.c
++++ b/src/doveadm/dsync/dsync-ibc-stream.c
+@@ -1492,12 +1492,12 @@ dsync_ibc_stream_send_mailbox_attribute(struct dsync_ibc *_ibc,
+ {
+       struct dsync_ibc_stream *ibc = (struct dsync_ibc_stream *)_ibc;
+       struct dsync_serializer_encoder *encoder;
+-      string_t *str = t_str_new(128);
+       char type[2];
+ 
+       if (ibc->minor_version < DSYNC_PROTOCOL_MINOR_HAVE_ATTRIBUTES)
+               return;
+ 
++      string_t *str = str_new(default_pool, 128);
+       str_append_c(str, items[ITEM_MAILBOX_ATTRIBUTE].chr);
+       encoder = dsync_ibc_send_encode_begin(ibc, ITEM_MAILBOX_ATTRIBUTE);
+ 
+@@ -1538,6 +1538,7 @@ dsync_ibc_stream_send_mailbox_attribute(struct dsync_ibc *_ibc,
+               i_stream_ref(ibc->value_output);
+               (void)dsync_ibc_stream_send_value_stream(ibc);
+       }
++      str_free(&str);
+ }
+ 
+ static enum dsync_ibc_recv_ret
+-- 
+2.47.3
+
diff --git a/patches/0001-global-Fix-spelling.patch b/patches/0001-global-Fix-spelling.patch
new file mode 100644 (file)
index 0000000..d16a996
--- /dev/null
@@ -0,0 +1,34 @@
+Index: trixie/src/lib-compression/istream-zlib.c
+===================================================================
+--- trixie.orig/src/lib-compression/istream-zlib.c
++++ trixie/src/lib-compression/istream-zlib.c
+@@ -32,7 +32,7 @@ struct zlib_istream {
+       bool header_read:1;
+       bool trailer_read:1;
+       bool zs_closed:1;
+-      bool starting_concated_output:1;
++      bool starting_concatenated_output:1;
+ };
+ 
+ static void i_stream_zlib_init(struct zlib_istream *zstream);
+@@ -193,9 +193,9 @@ static ssize_t i_stream_zlib_read(struct
+                       return -1;
+               }
+               /* Multiple gz streams concatenated together */
+-              zstream->starting_concated_output = TRUE;
++              zstream->starting_concatenated_output = TRUE;
+       }
+-      if (zstream->starting_concated_output) {
++      if (zstream->starting_concatenated_output) {
+               /* make sure there actually is something in parent stream.
+                  we don't want to reset the stream unless we actually see
+                  some concatenated output. */
+@@ -217,7 +217,7 @@ static ssize_t i_stream_zlib_read(struct
+               zstream->header_read = FALSE;
+               zstream->trailer_read = FALSE;
+               zstream->crc32 = 0;
+-              zstream->starting_concated_output = FALSE;
++              zstream->starting_concatenated_output = FALSE;
+ 
+               (void)inflateEnd(&zstream->zs);
+               i_stream_zlib_init(zstream);
diff --git a/patches/0001-imap-Extract-side-channel-ostream-creation-into-help.patch b/patches/0001-imap-Extract-side-channel-ostream-creation-into-help.patch
new file mode 100644 (file)
index 0000000..4470ae1
--- /dev/null
@@ -0,0 +1,82 @@
+From da6075cd4803a36ae06ae0401184aeed128e4320 Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Fri, 1 May 2026 17:40:08 +0000
+Subject: [PATCH 1/5] imap: Extract side-channel ostream creation into helper
+
+Add client_create_side_channel_output() and use it in cmd_compress()
+in place of the inline channel-creation code. The helper will also be
+used by the imap state import path to recreate the side channel after
+unhibernation. No functional change.
+---
+ src/imap/cmd-compress.c | 10 ++--------
+ src/imap/imap-client.c  |  9 +++++++++
+ src/imap/imap-client.h  |  5 +++++
+ 3 files changed, 16 insertions(+), 8 deletions(-)
+
+diff --git a/src/imap/cmd-compress.c b/src/imap/cmd-compress.c
+index 363d050f47..18aa988892 100644
+--- a/src/imap/cmd-compress.c
++++ b/src/imap/cmd-compress.c
+@@ -4,7 +4,6 @@
+ #include "imap-commands.h"
+ #include "istream.h"
+ #include "ostream.h"
+-#include "ostream-multiplex.h"
+ #include "iostream-rawlog.h"
+ #include "str.h"
+ #include "strescape.h"
+@@ -92,13 +91,8 @@ bool cmd_compress(struct client_command_context *cmd)
+               /* Let imap-login process handle the COMPRESS. It's the one
+                  that will send the tagged reply to the client. */
+               client->compress_handler = handler;
+-              if (client->side_channel_output == NULL) {
+-                      client->side_channel_output =
+-                              o_stream_multiplex_add_channel(
+-                                      client->multiplex_output, 1);
+-                      o_stream_set_no_error_handling(
+-                              client->side_channel_output, TRUE);
+-              }
++              if (client->side_channel_output == NULL)
++                      client_create_side_channel_output(client);
+               string_t *str = t_str_new(64);
+               str_append(str, "compress\t");
+               str_append_tabescaped(str, handler->name);
+diff --git a/src/imap/imap-client.c b/src/imap/imap-client.c
+index b65a8c9fdf..4c8115f9a0 100644
+--- a/src/imap/imap-client.c
++++ b/src/imap/imap-client.c
+@@ -678,6 +678,15 @@ void client_send_tagline(struct client_command_context *cmd, const char *data)
+       cmd->client->v.send_tagline(cmd, data);
+ }
+ 
++void client_create_side_channel_output(struct client *client)
++{
++      i_assert(client->multiplex_output != NULL);
++      i_assert(client->side_channel_output == NULL);
++      client->side_channel_output =
++              o_stream_multiplex_add_channel(client->multiplex_output, 1);
++      o_stream_set_no_error_handling(client->side_channel_output, TRUE);
++}
++
+ static void
+ client_default_send_tagline(struct client_command_context *cmd, const char *data)
+ {
+diff --git a/src/imap/imap-client.h b/src/imap/imap-client.h
+index fd1153d97f..a4f2e4f3fb 100644
+--- a/src/imap/imap-client.h
++++ b/src/imap/imap-client.h
+@@ -287,6 +287,11 @@ void client_add_istream_prefix(struct client *client,
+                              const unsigned char *data, size_t size);
+ void client_destroy(struct client *client, const char *reason) ATTR_NULL(2);
+ 
++/* Add the side-channel ostream used to send commands (e.g. dict_reset) back
++   to the imap-login proxy. Must only be called when multiplex_output is set
++   and the channel hasn't been created yet. */
++void client_create_side_channel_output(struct client *client);
++
+ /* Disconnect client connection */
+ void client_disconnect(struct client *client, const char *reason);
+ void client_disconnect_with_error(struct client *client,
+-- 
+2.47.3
+
diff --git a/patches/0001-imap-Restrict-COMPRESS-to-DEFLATE-only.patch b/patches/0001-imap-Restrict-COMPRESS-to-DEFLATE-only.patch
new file mode 100644 (file)
index 0000000..c3bfd4b
--- /dev/null
@@ -0,0 +1,67 @@
+From 18f076b165d480cfdf20d81b5683ce7929f02eb4 Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Sat, 4 Jul 2026 21:57:15 +0000
+Subject: [PATCH 1/3] imap: Restrict COMPRESS to DEFLATE only
+
+cmd_compress() and the imap-login proxy side channel looked up any
+registered compression handler by name, so a client could request e.g.
+COMPRESS ZSTD even though only COMPRESS=DEFLATE is advertised (RFC 4978).
+
+Non-DEFLATE algorithms can require far more memory to decompress (a zstd
+decoder alone can hold a 16+ MB window per connection versus ~40 KB for
+inflate). This matters now that imap-login handles COMPRESS for proxied
+connections: an attacker opening many connections could drive the shared
+login process into its vsz_limit and crash it, affecting all clients on
+that process.
+
+Only accept DEFLATE on both the backend and the login proxy path.
+---
+ src/imap-login/imap-proxy.c | 11 ++++++-----
+ src/imap/cmd-compress.c     |  8 ++++++--
+ 2 files changed, 12 insertions(+), 7 deletions(-)
+
+Index: trixie/src/imap-login/imap-proxy.c
+===================================================================
+--- trixie.orig/src/imap-login/imap-proxy.c
++++ trixie/src/imap-login/imap-proxy.c
+@@ -606,13 +606,14 @@ proxy_side_cmd_compress(struct client *c
+               login_proxy_get_client_ostream(client->login_proxy);
+       const struct compression_handler *handler;
+       int ret = compression_lookup_handler(t_str_lcase(args[0]), &handler);
+-      if (ret <= 0) {
+-              /* IMAP backend normally checks this already. If we get here,
+-                 there is a mismatch between what algorithms proxy and
+-                 backend supports. */
++      /* Only DEFLATE is permitted. The IMAP backend restricts this already;
++         enforce it here too, since imap-login handles COMPRESS for proxied
++         connections and non-DEFLATE algorithms can be far more memory-heavy
++         to decompress. */
++      if (ret <= 0 || strcmp(handler->name, "deflate") != 0) {
+               o_stream_nsend_str(client_output, t_strdup_printf(
+                       "%s NO %s compression mechanism\r\n", args[1],
+-                      ret == 0 ? "Unsupported" : "Unknown"));
++                      ret < 0 ? "Unknown" : "Unsupported"));
+               return 0;
+       }
+ 
+Index: trixie/src/imap/cmd-compress.c
+===================================================================
+--- trixie.orig/src/imap/cmd-compress.c
++++ trixie/src/imap/cmd-compress.c
+@@ -67,10 +67,14 @@ bool cmd_compress(struct client_command_
+               return TRUE;
+       }
+       int ret = compression_lookup_handler(t_str_lcase(mechanism), &handler);
+-      if (ret <= 0) {
++      /* Only DEFLATE is advertised (RFC 4978) and accepted here. Other
++         algorithms (e.g. zstd) can require far more memory to decompress,
++         which especially matters now that imap-login can handle COMPRESS for
++         proxied connections and an attacker could open many of them. */
++      if (ret <= 0 || strcmp(handler->name, "deflate") != 0) {
+               const char * tagline =
+                       t_strdup_printf("NO %s compression mechanism",
+-                                      ret == 0 ? "Unsupported" : "Unknown");
++                                      ret < 0 ? "Unknown" : "Unsupported");
+               client_send_tagline(cmd, tagline);
+               return TRUE;
+       }
diff --git a/patches/0001-imap-hibernate-Fix-out-of-bounds-read-when-parsing-D.patch b/patches/0001-imap-hibernate-Fix-out-of-bounds-read-when-parsing-D.patch
new file mode 100644 (file)
index 0000000..846b310
--- /dev/null
@@ -0,0 +1,35 @@
+From b4da247c728ecddc125c2d4021b9b7f70ff1f549 Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Thu, 16 Apr 2026 23:12:13 +0200
+Subject: [PATCH 1/2] imap-hibernate: Fix out-of-bounds read when parsing DONE
+ command tag
+
+The tag-skipping loop was missing a size>0 guard, so a malformed
+DONE command with no space/CR/tab terminator after the tag would
+read one byte past the end of the buffer. Also add a \0 check to
+stop on embedded null bytes, which are not valid tag characters.
+---
+ src/imap-hibernate/imap-client.c | 6 ++++--
+ 1 file changed, 4 insertions(+), 2 deletions(-)
+
+diff --git a/src/imap-hibernate/imap-client.c b/src/imap-hibernate/imap-client.c
+index 5c11dd1dde..2110fe8e8e 100644
+--- a/src/imap-hibernate/imap-client.c
++++ b/src/imap-hibernate/imap-client.c
+@@ -362,9 +362,11 @@ imap_client_input_parse(const unsigned char *data, size_t size, const char **tag
+       tag_start = data;
+ 
+       /* skip over tag */
+-      while(data[0] != ' ' &&
++      while(size > 0 &&
++            data[0] != ' ' &&
+             data[0] != '\r' &&
+-            data[0] != '\t' ) { data++; size--; }
++            data[0] != '\t' &&
++            data[0] != '\0') { data++; size--; }
+ 
+       tag_end = data;
+ 
+-- 
+2.47.3
+
diff --git a/patches/0001-imap-login-Add-comments-to-internal-ID-command-param.patch b/patches/0001-imap-login-Add-comments-to-internal-ID-command-param.patch
new file mode 100644 (file)
index 0000000..c714df5
--- /dev/null
@@ -0,0 +1,33 @@
+From 277e5c880a7f6a7741e717036570812910fff7b7 Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Mon, 2 Feb 2026 11:44:58 +0200
+Subject: [PATCH] imap-login: Add comments to internal ID command parameter
+ handling
+
+---
+ src/imap-login/imap-login-cmd-id.c | 9 +++++++--
+ 1 file changed, 7 insertions(+), 2 deletions(-)
+
+diff --git a/src/imap-login/imap-login-cmd-id.c b/src/imap-login/imap-login-cmd-id.c
+index 1cd30bd1bc..e51e9c1e72 100644
+--- a/src/imap-login/imap-login-cmd-id.c
++++ b/src/imap-login/imap-login-cmd-id.c
+@@ -183,8 +183,13 @@ static bool cmd_id_handle_keyvalue(struct imap_client *client,
+               imap_id_param_handler_find(key);
+       bool is_login_id_param = handler != NULL;
+ 
+-      if (is_login_id_param && client->common.connection_trusted &&
+-          !client->id_logged && value != NULL) {
++      if (!is_login_id_param) {
++              /* not an internal key */
++      } else if (client->id_logged) {
++              /* using ID multiple times - ignore */
++      } else if (value == NULL) {
++              /* there should have been a value - ignore */
++      } else if (client->common.connection_trusted) {
+               if (!handler->callback(client->cmd_id->params, key, value)) {
+                       e_debug(client->common.event,
+                               "Client sent invalid ID parameter '%s'", key);
+-- 
+2.47.3
+
diff --git a/patches/0001-imap-login-Fix-excessive-memory-growth-with-pre-logi.patch b/patches/0001-imap-login-Fix-excessive-memory-growth-with-pre-logi.patch
new file mode 100644 (file)
index 0000000..12289ee
--- /dev/null
@@ -0,0 +1,86 @@
+From 4f3471057a754d93af368527e08af3db3d3e82c9 Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Sat, 4 Jul 2026 16:04:04 +0000
+Subject: [PATCH] imap-login: Fix excessive memory growth with pre-login ID
+ command
+
+The pre-login ID parser reset the per-argument line and list limits after
+every argument, so nothing bounded the number of key/value pairs. Each
+external key grew log_reply and added a distinct event field (a linear
+scan, making it O(N^2)), allowing a pre-auth CPU/memory amplification.
+
+Account at most 30 pairs (per RFC 2971) into the logging and client_id
+bookkeeping. Internal x-* parameter handlers still run for pairs beyond
+the limit, since a trusted proxy may legitimately forward more fields.
+---
+ src/imap-login/imap-login-client.h |  4 ++++
+ src/imap-login/imap-login-cmd-id.c | 20 ++++++++++++++++++--
+ 2 files changed, 22 insertions(+), 2 deletions(-)
+
+Index: trixie/src/imap-login/imap-login-client.h
+===================================================================
+--- trixie.orig/src/imap-login/imap-login-client.h
++++ trixie/src/imap-login/imap-login-client.h
+@@ -53,6 +53,10 @@ struct imap_client_cmd_id {
+       struct event *params_event;
+       struct imap_id_params *params;
+       string_t *log_reply;
++      /* Number of external (non internal x-*) key/value pairs accounted so
++         far. Used to bound the pre-login accounting (logging, event fields,
++         client_id) done per external pair. */
++      unsigned int processed_pairs_count;
+ };
+ 
+ struct imap_client {
+Index: trixie/src/imap-login/imap-login-cmd-id.c
+===================================================================
+--- trixie.orig/src/imap-login/imap-login-cmd-id.c
++++ trixie/src/imap-login/imap-login-cmd-id.c
+@@ -9,6 +9,12 @@
+ #include "imap-login-settings.h"
+ #include "imap-login-client.h"
+ 
++/* RFC 2971 says a client SHOULD NOT send more than 30 field-value pairs.
++   Allow that many to be accounted for (logged / kept in client_id); pairs
++   beyond this are still parsed and internal x-* handlers still run, but they
++   are not added to the logging/client_id accounting that grows per pair. */
++#define IMAP_ID_MAX_ACCOUNTED_PAIRS 30
++
+ struct imap_id_params_forward {
+       const char *key;
+       const char *value;
+@@ -183,6 +189,16 @@ static bool cmd_id_handle_keyvalue(struc
+               imap_id_param_handler_find(key);
+       bool is_login_id_param = handler != NULL;
+ 
++      /* Only external pairs are logged / kept in client_id, and only those
++         grow per-pair with count, so only they need bounding to avoid a
++         pre-login CPU/memory DoS. Internal x-* handlers must keep running
++         regardless (e.g. a proxy may legitimately forward more than 30
++         fields) and are not counted against the limit. */
++      bool account_pair = !is_login_id_param &&
++              client->cmd_id->processed_pairs_count < IMAP_ID_MAX_ACCOUNTED_PAIRS;
++      if (account_pair)
++              client->cmd_id->processed_pairs_count++;
++
+       if (!is_login_id_param) {
+               /* not an internal key */
+       } else if (client->id_logged) {
+@@ -197,7 +213,7 @@ static bool cmd_id_handle_keyvalue(struc
+               }
+       }
+ 
+-      if (client->set->imap_id_retain && !is_login_id_param &&
++      if (account_pair && client->set->imap_id_retain &&
+           (client->common.client_id == NULL ||
+            str_len(client->common.client_id) + kvlen < LOGIN_MAX_CLIENT_ID_LEN)) {
+               if (client->common.client_id == NULL) {
+@@ -213,7 +229,7 @@ static bool cmd_id_handle_keyvalue(struc
+                       imap_append_quoted(client->common.client_id, value);
+       }
+ 
+-      if (!is_login_id_param)
++      if (account_pair)
+               imap_id_add_log_entry(log_entry, key, value);
+       return TRUE;
+ }
diff --git a/patches/0001-lda-Fix-using-USER-environment-if-d-hasn-t-been-spec.patch b/patches/0001-lda-Fix-using-USER-environment-if-d-hasn-t-been-spec.patch
new file mode 100644 (file)
index 0000000..2d55416
--- /dev/null
@@ -0,0 +1,38 @@
+From 0d490d6453b4779bc711f1b998e6d09ae2aab8a8 Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Mon, 26 May 2025 09:37:35 +0300
+Subject: [PATCH] lda: Fix using USER environment if -d hasn't been specified
+
+This became broken at some point.
+---
+ src/lda/main.c | 10 +++++++---
+ 1 file changed, 7 insertions(+), 3 deletions(-)
+
+Index: dovecot/src/lda/main.c
+===================================================================
+--- dovecot.orig/src/lda/main.c
++++ dovecot/src/lda/main.c
+@@ -453,9 +453,6 @@ int main(int argc, char *argv[])
+               i_fatal_status(EX_USAGE, "Unknown argument: %s", argv[optind]);
+       }
+ 
+-      if (master_service_settings_read_simple(master_service, &error) < 0)
+-              i_fatal("%s", error);
+-
+       process_euid = geteuid();
+       if ((service_flags & MAIL_STORAGE_SERVICE_FLAG_USERDB_LOOKUP) != 0)
+               ;
+@@ -485,6 +482,13 @@ int main(int argc, char *argv[])
+               i_fatal_status(EX_USAGE,
+                       "destination user parameter (-d user) not given");
+       }
++      struct master_service_settings_input set_input = {
++              .preserve_user = TRUE,
++      };
++      struct master_service_settings_output set_output;
++      if (master_service_settings_read(master_service, &set_input,
++                                       &set_output, &error) < 0)
++              i_fatal("%s", error);
+       master_service_init_finish(master_service);
+ 
+       dinput.mail_from = mail_from;
diff --git a/patches/0001-lib-Add-XXH64-hash-implementation.patch b/patches/0001-lib-Add-XXH64-hash-implementation.patch
new file mode 100644 (file)
index 0000000..3584708
--- /dev/null
@@ -0,0 +1,331 @@
+From 972acc9f9e73a8bc5b029991fb3752d9d16ef632 Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Mon, 13 Apr 2026 12:30:03 +0200
+Subject: [PATCH 01/12] lib: Add XXH64 hash implementation
+
+Adds a streaming XXH64 (non-cryptographic) hash with init/loop/result
+API, a one-shot xxh64_data(), and an xxh64_to_32() XOR-fold inline.
+Registered in hash_methods[] for use via the generic hash_method API.
+---
+ src/lib/Makefile.am        |   2 +
+ src/lib/hash-method.c      |   2 +
+ src/lib/test-hash-method.c |  33 +++++++
+ src/lib/xxh64.c            | 184 +++++++++++++++++++++++++++++++++++++
+ src/lib/xxh64.h            |  30 ++++++
+ 5 files changed, 251 insertions(+)
+ create mode 100644 src/lib/xxh64.c
+ create mode 100644 src/lib/xxh64.h
+
+diff --git a/src/lib/Makefile.am b/src/lib/Makefile.am
+index 6da8a308e5..eb9b9ddda3 100644
+--- a/src/lib/Makefile.am
++++ b/src/lib/Makefile.am
+@@ -177,6 +177,7 @@ liblib_la_SOURCES = \
+       sha1.c \
+       sha2.c \
+       sha3.c \
++      xxh64.c \
+       sleep.c \
+       sort.c \
+       stats-dist.c \
+@@ -337,6 +338,7 @@ headers = \
+       sha1.h \
+       sha2.h \
+       sha3.h \
++      xxh64.h \
+       sleep.h \
+       sort.h \
+       stats-dist.h \
+diff --git a/src/lib/hash-method.c b/src/lib/hash-method.c
+index d7a6bed0ea..bdeefbadc6 100644
+--- a/src/lib/hash-method.c
++++ b/src/lib/hash-method.c
+@@ -6,6 +6,7 @@
+ #include "sha1.h"
+ #include "sha2.h"
+ #include "sha3.h"
++#include "xxh64.h"
+ #include "hash-method.h"
+ 
+ const struct hash_method *hash_method_lookup(const char *name)
+@@ -93,6 +94,7 @@ const struct hash_method *hash_methods[] = {
+       &hash_method_sha512,
+       &hash_method_sha3_256,
+       &hash_method_sha3_512,
++      &hash_method_xxh64,
+       &hash_method_size,
+       NULL
+ };
+diff --git a/src/lib/test-hash-method.c b/src/lib/test-hash-method.c
+index 6592ec90e6..6384dc99ca 100644
+--- a/src/lib/test-hash-method.c
++++ b/src/lib/test-hash-method.c
+@@ -425,6 +425,39 @@ static void test_hash_methods_fips() {
+                       "\x6d\xf6\x54\x5a\x1c\xe8\xba\x00",
+                       512 / 8,
+               },
++              /* xxh64 reference vectors (seed=0) */
++              { "xxh64",
++                      "",
++                      0,
++                      1,
++                      "\xef\x46\xdb\x37\x51\xd8\xe9\x99",
++                      64 / 8
++              },
++              { "xxh64",
++                      "abc",
++                      3,
++                      1,
++                      "\x44\xbc\x2c\xf5\xad\x77\x09\x99",
++                      64 / 8
++              },
++              /* 62 bytes: exercises the >= 32-byte accumulator path */
++              { "xxh64",
++                      "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef"
++                      "ghijklmnopqrstuvwxyz0123456789",
++                      62,
++                      1,
++                      "\xba\x76\x1a\x9a\xa8\x0e\x0d\x36",
++                      64 / 8
++              },
++              /* 16 bytes Ã— 3 rounds: exercises chunked input across the 32-byte block boundary */
++              { "xxh64",
++                      "\x00\x01\x02\x03\x04\x05\x06\x07"
++                      "\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f",
++                      16,
++                      3,
++                      "\x32\x0d\xcd\xab\xd8\x29\x7a\x9e",
++                      64 / 8
++              },
+       };
+ 
+       for(size_t i = 0; i < N_ELEMENTS(test_vectors); i++) {
+diff --git a/src/lib/xxh64.c b/src/lib/xxh64.c
+new file mode 100644
+index 0000000000..1c17833afd
+--- /dev/null
++++ b/src/lib/xxh64.c
+@@ -0,0 +1,184 @@
++/* Copyright (c) 2025 Dovecot authors, see the included COPYING file */
++
++/* XXHash64 - Fast non-cryptographic hash function.
++ * Based on Yann Collet's public domain XXHash64 algorithm.
++ * Reference: https://github.com/Cyan4973/xxHash/blob/dev/doc/xxhash_spec.md
++ */
++
++#include "lib.h"
++#include "xxh64.h"
++
++#define XXH64_PRIME1  UINT64_C(0x9E3779B185EBCA87)
++#define XXH64_PRIME2  UINT64_C(0xC2B2AE3D27D4EB4F)
++#define XXH64_PRIME3  UINT64_C(0x165667B19E3779F9)
++#define XXH64_PRIME4  UINT64_C(0x85EBCA77C2B27589)
++#define XXH64_PRIME5  UINT64_C(0x27D4EB2F165667C5)
++
++#define XXH64_ROTL(x, r) (((x) << (r)) | ((x) >> (64 - (r))))
++
++static inline uint64_t xxh64_read64(const void *p)
++{
++      uint64_t v;
++      memcpy(&v, p, sizeof(v));
++      return v;
++}
++
++static inline uint32_t xxh64_read32(const void *p)
++{
++      uint32_t v;
++      memcpy(&v, p, sizeof(v));
++      return v;
++}
++
++static uint64_t ATTR_UNSIGNED_WRAPS xxh64_round(uint64_t acc, uint64_t input)
++{
++      acc += input * XXH64_PRIME2;
++      acc = XXH64_ROTL(acc, 31);
++      acc *= XXH64_PRIME1;
++      return acc;
++}
++
++static uint64_t ATTR_UNSIGNED_WRAPS xxh64_merge_acc(uint64_t h64, uint64_t acc)
++{
++      acc = xxh64_round(0, acc);
++      h64 ^= acc;
++      h64 = h64 * XXH64_PRIME1 + XXH64_PRIME4;
++      return h64;
++}
++
++void ATTR_UNSIGNED_WRAPS xxh64_init(struct xxh64_context *ctx, uint64_t seed)
++{
++      ctx->seed = seed;
++      ctx->v1 = seed + XXH64_PRIME1 + XXH64_PRIME2;
++      ctx->v2 = seed + XXH64_PRIME2;
++      ctx->v3 = seed;
++      ctx->v4 = seed - XXH64_PRIME1;
++      ctx->total_len = 0;
++      ctx->buf_used = 0;
++}
++
++void ATTR_UNSIGNED_WRAPS xxh64_loop(struct xxh64_context *ctx, const void *data, size_t size)
++{
++      const unsigned char *p = data;
++      const unsigned char *end = p + size;
++
++      ctx->total_len += size;
++
++      if (ctx->buf_used + size < 32) {
++              memcpy(ctx->buf + ctx->buf_used, p, size);
++              ctx->buf_used += size;
++              return;
++      }
++
++      if (ctx->buf_used > 0) {
++              size_t fill = 32 - ctx->buf_used;
++              memcpy(ctx->buf + ctx->buf_used, p, fill);
++              p += fill;
++              ctx->v1 = xxh64_round(ctx->v1, xxh64_read64(ctx->buf));
++              ctx->v2 = xxh64_round(ctx->v2, xxh64_read64(ctx->buf + 8));
++              ctx->v3 = xxh64_round(ctx->v3, xxh64_read64(ctx->buf + 16));
++              ctx->v4 = xxh64_round(ctx->v4, xxh64_read64(ctx->buf + 24));
++              ctx->buf_used = 0;
++      }
++
++      while (p + 32 <= end) {
++              ctx->v1 = xxh64_round(ctx->v1, xxh64_read64(p));
++              ctx->v2 = xxh64_round(ctx->v2, xxh64_read64(p + 8));
++              ctx->v3 = xxh64_round(ctx->v3, xxh64_read64(p + 16));
++              ctx->v4 = xxh64_round(ctx->v4, xxh64_read64(p + 24));
++              p += 32;
++      }
++
++      if (p < end) {
++              ctx->buf_used = (unsigned int)(end - p);
++              memcpy(ctx->buf, p, ctx->buf_used);
++      }
++}
++
++uint64_t ATTR_UNSIGNED_WRAPS xxh64_result(struct xxh64_context *ctx)
++{
++      const unsigned char *p = ctx->buf;
++      const unsigned char *end = p + ctx->buf_used;
++      uint64_t h64;
++
++      if (ctx->total_len >= 32) {
++              h64 = XXH64_ROTL(ctx->v1,  1) + XXH64_ROTL(ctx->v2,  7) +
++                    XXH64_ROTL(ctx->v3, 12) + XXH64_ROTL(ctx->v4, 18);
++              h64 = xxh64_merge_acc(h64, ctx->v1);
++              h64 = xxh64_merge_acc(h64, ctx->v2);
++              h64 = xxh64_merge_acc(h64, ctx->v3);
++              h64 = xxh64_merge_acc(h64, ctx->v4);
++      } else {
++              h64 = ctx->seed + XXH64_PRIME5;
++      }
++
++      h64 += ctx->total_len;
++
++      while (p + 8 <= end) {
++              h64 ^= xxh64_round(0, xxh64_read64(p));
++              h64 = XXH64_ROTL(h64, 27) * XXH64_PRIME1 + XXH64_PRIME4;
++              p += 8;
++      }
++      if (p + 4 <= end) {
++              h64 ^= (uint64_t)xxh64_read32(p) * XXH64_PRIME1;
++              h64 = XXH64_ROTL(h64, 23) * XXH64_PRIME2 + XXH64_PRIME3;
++              p += 4;
++      }
++      while (p < end) {
++              h64 ^= (uint64_t)*p * XXH64_PRIME5;
++              h64 = XXH64_ROTL(h64, 11) * XXH64_PRIME1;
++              p++;
++      }
++
++      /* Avalanche */
++      h64 ^= h64 >> 33;
++      h64 *= XXH64_PRIME2;
++      h64 ^= h64 >> 29;
++      h64 *= XXH64_PRIME3;
++      h64 ^= h64 >> 32;
++
++      return h64;
++}
++
++uint64_t xxh64_data(const void *data, size_t size, uint64_t seed)
++{
++      struct xxh64_context ctx;
++
++      xxh64_init(&ctx, seed);
++      xxh64_loop(&ctx, data, size);
++      return xxh64_result(&ctx);
++}
++
++static void xxh64_hash_init(void *context)
++{
++      xxh64_init(context, 0);
++}
++
++static void xxh64_hash_loop(void *context, const void *data, size_t size)
++{
++      xxh64_loop(context, data, size);
++}
++
++static void xxh64_hash_result(void *context, unsigned char *digest_r)
++{
++      uint64_t hash = xxh64_result(context);
++
++      digest_r[0] = (hash >> 56) & 0xff;
++      digest_r[1] = (hash >> 48) & 0xff;
++      digest_r[2] = (hash >> 40) & 0xff;
++      digest_r[3] = (hash >> 32) & 0xff;
++      digest_r[4] = (hash >> 24) & 0xff;
++      digest_r[5] = (hash >> 16) & 0xff;
++      digest_r[6] = (hash >>  8) & 0xff;
++      digest_r[7] = (hash >>  0) & 0xff;
++}
++
++const struct hash_method hash_method_xxh64 = {
++      .name = "xxh64",
++      .block_size = 32,
++      .context_size = sizeof(struct xxh64_context),
++      .digest_size = XXH64_RESULTLEN,
++      .init = xxh64_hash_init,
++      .loop = xxh64_hash_loop,
++      .result = xxh64_hash_result,
++};
+diff --git a/src/lib/xxh64.h b/src/lib/xxh64.h
+new file mode 100644
+index 0000000000..7beb4cafb4
+--- /dev/null
++++ b/src/lib/xxh64.h
+@@ -0,0 +1,30 @@
++#ifndef XXH64_H
++#define XXH64_H
++
++#include "hash-method.h"
++
++#define XXH64_RESULTLEN 8
++
++struct xxh64_context {
++      uint64_t seed;
++      uint64_t v1, v2, v3, v4;
++      uint64_t total_len;
++      unsigned char buf[32];
++      unsigned int buf_used;
++};
++
++void xxh64_init(struct xxh64_context *ctx, uint64_t seed);
++void xxh64_loop(struct xxh64_context *ctx, const void *data, size_t size);
++uint64_t xxh64_result(struct xxh64_context *ctx);
++
++uint64_t xxh64_data(const void *data, size_t size, uint64_t seed) ATTR_PURE;
++
++/* XOR-fold the 64-bit hash to 32 bits */
++static inline uint32_t xxh64_to_32(uint64_t hash)
++{
++      return (uint32_t)(hash ^ (hash >> 32));
++}
++
++extern const struct hash_method hash_method_xxh64;
++
++#endif
+-- 
+2.47.3
+
diff --git a/patches/0001-lib-Add-str_equals_timing_safe.patch b/patches/0001-lib-Add-str_equals_timing_safe.patch
new file mode 100644 (file)
index 0000000..7a6c53f
--- /dev/null
@@ -0,0 +1,124 @@
+From 09237b986e2a5710a562a9750a6df1aef6d28e69 Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Tue, 28 Apr 2026 01:50:56 +0300
+Subject: [PATCH 1/2] lib: Add str_equals_timing_safe()
+
+Constant-time string comparison that avoids the length leak in
+str_equals_timing_almost_safe(). Compares HMAC-SHA256 digests of the
+inputs (keyed with hash_iv) rather than the strings themselves, so
+neither the contents nor the length of either input affects timing in
+a way an attacker can exploit.
+---
+ src/lib/strfuncs.c      | 29 +++++++++++++++++++++++++++++
+ src/lib/strfuncs.h      |  5 +++++
+ src/lib/test-strfuncs.c | 27 +++++++++++++++++++++++++++
+ 3 files changed, 61 insertions(+)
+
+Index: dovecot/src/lib/strfuncs.c
+===================================================================
+--- dovecot.orig/src/lib/strfuncs.c
++++ dovecot/src/lib/strfuncs.c
+@@ -7,6 +7,9 @@
+ #include "printf-format-fix.h"
+ #include "strfuncs.h"
+ #include "array.h"
++#include "hash.h"
++#include "hmac.h"
++#include "sha2.h"
+ 
+ #include <stdio.h>
+ #include <limits.h>
+@@ -627,6 +630,32 @@ bool str_equals_timing_almost_safe(const
+       return ret == 0;
+ }
+ 
++bool str_equals_hash_timing_safe(const char *s1, const char *s2)
++{
++      struct hmac_context ctx;
++      unsigned char digest1[SHA256_RESULTLEN];
++      unsigned char digest2[SHA256_RESULTLEN];
++
++      /* Compare HMAC-SHA256 digests of the inputs rather than the inputs
++         themselves. The digest length is constant, so the subsequent
++         mem_equals_timing_safe() leaks no length information. The HMAC
++         times depend on the input lengths, but each side's length is either
++         a deployment constant (for the secret) or already known to the
++         attacker (for their own input), so neither leaks a useful signal.
++         hash_iv keys the HMAC to prevent precomputation. */
++      hmac_init(&ctx, (const unsigned char *)&hash_iv, sizeof(hash_iv),
++                &hash_method_sha256);
++      hmac_update(&ctx, s1, strlen(s1));
++      hmac_final(&ctx, digest1);
++
++      hmac_init(&ctx, (const unsigned char *)&hash_iv, sizeof(hash_iv),
++                &hash_method_sha256);
++      hmac_update(&ctx, s2, strlen(s2));
++      hmac_final(&ctx, digest2);
++
++      return mem_equals_timing_safe(digest1, digest2, sizeof(digest1));
++}
++
+ size_t
+ str_match(const char *p1, const char *p2)
+ {
+Index: dovecot/src/lib/strfuncs.h
+===================================================================
+--- dovecot.orig/src/lib/strfuncs.h
++++ dovecot/src/lib/strfuncs.h
+@@ -93,6 +93,11 @@ bool mem_equals_timing_safe(const void *
+    the string lengths are the same. If not, the length of the secret string may
+    be leaked, but otherwise the contents won't be. */
+ bool str_equals_timing_almost_safe(const char *s1, const char *s2);
++/* Returns TRUE if the two strings are equal. Safe against timing attacks:
++   neither the contents nor the length of either string is leaked.
++   Implemented by HMAC-SHA256ing both inputs under a random per-process key
++   and comparing the fixed-length digests. */
++bool str_equals_hash_timing_safe(const char *s1, const char *s2);
+ 
+ size_t str_match(const char *p1, const char *p2) ATTR_PURE;
+ size_t str_match_icase(const char *p1, const char *p2) ATTR_PURE;
+Index: dovecot/src/lib/test-strfuncs.c
+===================================================================
+--- dovecot.orig/src/lib/test-strfuncs.c
++++ dovecot/src/lib/test-strfuncs.c
+@@ -478,6 +478,32 @@ static void test_str_equals_timing_almos
+       test_end();
+ }
+ 
++static void test_str_equals_hash_timing_safe(void)
++{
++      const struct {
++              const char *a, *b;
++      } tests[] = {
++              { "", "" },
++              { "a", "a" },
++              { "b", "a" },
++              { "ab", "ab" },
++              { "ab", "ba" },
++              { "ab", "bc" },
++              { "a", "" },
++              { "a", "ab" },
++              { "a", "abc" },
++              { "ab", "abc" },
++      };
++      test_begin("str_equals_hash_timing_safe()");
++      for (unsigned int i = 0; i < N_ELEMENTS(tests); i++) {
++              test_assert((strcmp(tests[i].a, tests[i].b) == 0) ==
++                          str_equals_hash_timing_safe(tests[i].a, tests[i].b));
++              test_assert((strcmp(tests[i].a, tests[i].b) == 0) ==
++                          str_equals_hash_timing_safe(tests[i].b, tests[i].a));
++      }
++      test_end();
++}
++
+ static void test_dec2str_buf(void)
+ {
+       const uintmax_t test_input[] = {
+@@ -773,6 +799,7 @@ void test_strfuncs(void)
+       test_p_array_const_string_join();
+       test_mem_equals_timing_safe();
+       test_str_equals_timing_almost_safe();
++      test_str_equals_hash_timing_safe();
+       test_dec2str_buf();
+       test_str_match();
+       test_str_match_icase();
diff --git a/patches/0001-lib-compression-Don-t-calculate-crc32-if-using-zlib-.patch b/patches/0001-lib-compression-Don-t-calculate-crc32-if-using-zlib-.patch
new file mode 100644 (file)
index 0000000..6f07d05
--- /dev/null
@@ -0,0 +1,45 @@
+From cac140a9e4e12485bde4348b8a8c94a7a96a6e5e Mon Sep 17 00:00:00 2001
+From: Michael M Slusarz <michael.slusarz@open-xchange.com>
+Date: Sat, 4 Oct 2025 12:04:31 -0600
+Subject: [PATCH] lib-compression: Don't calculate crc32 if using zlib in
+ deflate mode
+
+---
+ src/lib-compression/istream-zlib.c | 8 +++++---
+ src/lib-compression/ostream-zlib.c | 4 +++-
+ 2 files changed, 8 insertions(+), 4 deletions(-)
+
+Index: trixie/src/lib-compression/istream-zlib.c
+===================================================================
+--- trixie.orig/src/lib-compression/istream-zlib.c
++++ trixie/src/lib-compression/istream-zlib.c
+@@ -264,9 +264,11 @@ static ssize_t i_stream_zlib_read(struct
+       ret = inflate(&zstream->zs, Z_SYNC_FLUSH);
+ 
+       out_size -= zstream->zs.avail_out;
+-      zstream->crc32 = crc32_data_more(zstream->crc32,
+-                                       stream->w_buffer + stream->pos,
+-                                       out_size);
++      /* CRC32 is only needed for GZ trailer. */
++      if (zstream->gz)
++              zstream->crc32 = crc32_data_more(zstream->crc32,
++                                               stream->w_buffer + stream->pos,
++                                               out_size);
+       stream->pos += out_size;
+ 
+       size_t bytes_consumed = size - zstream->zs.avail_in;
+Index: trixie/src/lib-compression/ostream-zlib.c
+===================================================================
+--- trixie.orig/src/lib-compression/ostream-zlib.c
++++ trixie/src/lib-compression/ostream-zlib.c
+@@ -219,7 +219,9 @@ o_stream_zlib_send_chunk(struct zlib_ost
+       }
+       size -= zs->avail_in;
+ 
+-      zstream->crc = crc32_data_more(zstream->crc, data, size);
++      /* CRC32 is only needed for GZ trailer. */
++      if (zstream->gz)
++              zstream->crc = crc32_data_more(zstream->crc, data, size);
+       zstream->bytes32 += size;
+       zstream->flushed = FALSE;
+       return size;
diff --git a/patches/0001-lib-compression-istream-lz4-Ensure-uncompressed-chun.patch b/patches/0001-lib-compression-istream-lz4-Ensure-uncompressed-chun.patch
new file mode 100644 (file)
index 0000000..dcbc6b6
--- /dev/null
@@ -0,0 +1,98 @@
+From 86aba25a1159f7ff11480feec287647be78054d1 Mon Sep 17 00:00:00 2001
+From: Aki Tuomi <aki.tuomi@open-xchange.com>
+Date: Mon, 23 Feb 2026 13:52:36 +0200
+Subject: [PATCH] lib-compression: istream-lz4 - Ensure uncompressed chunk size
+ is not 0
+
+---
+ src/lib-compression/istream-lz4.c      |  5 ++-
+ src/lib-compression/test-compression.c | 52 ++++++++++++++++++++++++++
+ 2 files changed, 55 insertions(+), 2 deletions(-)
+
+Index: trixie/src/lib-compression/istream-lz4.c
+===================================================================
+--- trixie.orig/src/lib-compression/istream-lz4.c
++++ trixie/src/lib-compression/istream-lz4.c
+@@ -77,9 +77,10 @@ static int i_stream_lz4_read_header(stru
+       zstream->max_uncompressed_chunk_size =
+               be32_to_cpu_unaligned(hdr->max_uncompressed_chunk_size);
+       buffer_set_used_size(zstream->chunk_buf, 0);
+-      if (zstream->max_uncompressed_chunk_size > ISTREAM_LZ4_CHUNK_SIZE) {
++      if (zstream->max_uncompressed_chunk_size  == 0 ||
++          zstream->max_uncompressed_chunk_size > ISTREAM_LZ4_CHUNK_SIZE) {
+               lz4_read_error(zstream, t_strdup_printf(
+-                      "lz4 max chunk size too large (%u > %u)",
++                      "invalid lz4 max chunk size (%u, max %u)",
+                       zstream->max_uncompressed_chunk_size,
+                       ISTREAM_LZ4_CHUNK_SIZE));
+               zstream->istream.istream.stream_errno = EINVAL;
+Index: trixie/src/lib-compression/test-compression.c
+===================================================================
+--- trixie.orig/src/lib-compression/test-compression.c
++++ trixie/src/lib-compression/test-compression.c
+@@ -1040,6 +1040,57 @@ static void test_lz4_small_header(void)
+       test_end();
+ }
+ 
++static const unsigned char lz4_chunk_crash_01[] = {
++       0x44, 0x6f, 0x76, 0x65, 0x63, 0x6f, 0x74, 0x2d, 0x4c, 0x5a, 0x34, 0x0d,
++       0x2a, 0x9b, 0xc5, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x7a,
++       0x32, 0x01, 0xff, 0xff, 0x00, 0x30
++};
++
++static const unsigned char lz4_chunk_crash_02[] = {
++       0x44, 0x6f, 0x76, 0x65, 0x63, 0x6f, 0x74, 0x2d, 0x4c, 0x5a, 0x34, 0x0d,
++       0x2a, 0x9b, 0xc5, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x07, 0x7a,
++       0x32, 0x01, 0xff, 0xff, 0x00, 0x30
++};
++
++static void test_lz4_chunk_size(void)
++{
++      const struct compression_handler *lz4;
++      struct istream *file_input, *input;
++
++      if (compression_lookup_handler("lz4", &lz4) <= 0)
++              return; /* not compiled in or unknown */
++
++      test_begin("lz4 chunk size");
++
++      file_input = test_istream_create_data(lz4_chunk_crash_01,
++                                            sizeof(lz4_chunk_crash_01));
++      input = lz4->create_istream(file_input);
++      i_stream_unref(&file_input);
++      i_stream_read(input);
++
++      test_assert(input->eof);
++      test_assert(input->stream_errno != 0);
++      const char *error = i_stream_get_error(input);
++      test_assert(strstr(error, "invalid lz4 max chunk size") != NULL);
++
++      i_stream_unref(&input);
++
++      file_input = test_istream_create_data(lz4_chunk_crash_02,
++                                            sizeof(lz4_chunk_crash_02));
++      input = lz4->create_istream(file_input);
++      i_stream_unref(&file_input);
++      i_stream_read(input);
++
++      test_assert(input->eof);
++      test_assert(input->stream_errno != 0);
++      error = i_stream_get_error(input);
++      test_assert(strstr(error, "invalid lz4 max chunk size") != NULL);
++
++      i_stream_unref(&input);
++
++      test_end();
++}
++
+ static void test_uncompress_file(const char *path)
+ {
+       const struct compression_handler *handler;
+@@ -1163,6 +1214,7 @@ int main(int argc, char *argv[])
+               test_gz_header,
+               test_gz_large_header,
+               test_lz4_small_header,
++              test_lz4_chunk_size,
+               test_compression_ext,
+               test_compression_deinit,
+               NULL
diff --git a/patches/0001-lib-compression-istream-lz4-Try-again-if-no-data-was.patch b/patches/0001-lib-compression-istream-lz4-Try-again-if-no-data-was.patch
new file mode 100644 (file)
index 0000000..dd97d3f
--- /dev/null
@@ -0,0 +1,60 @@
+From f269b3d3051044aec31cef888fcf4c9ddd21c227 Mon Sep 17 00:00:00 2001
+From: Aki Tuomi <aki.tuomi@open-xchange.com>
+Date: Mon, 23 Feb 2026 14:09:02 +0200
+Subject: [PATCH] lib-compression: istream-lz4 - Try again if no data was
+ decompressed
+
+---
+ src/lib-compression/istream-lz4.c      |  3 ++-
+ src/lib-compression/test-compression.c | 17 +++++++++++++++++
+ 2 files changed, 19 insertions(+), 1 deletion(-)
+
+Index: trixie/src/lib-compression/istream-lz4.c
+===================================================================
+--- trixie.orig/src/lib-compression/istream-lz4.c
++++ trixie/src/lib-compression/istream-lz4.c
+@@ -198,7 +198,8 @@ static ssize_t i_stream_lz4_read(struct
+               lz4_read_error(zstream, "corrupted lz4 chunk");
+               stream->istream.stream_errno = EINVAL;
+               return -1;
+-      }
++      } else if (ret == 0)
++              return i_stream_lz4_read(stream);
+       i_assert(ret > 0);
+       stream->pos += ret;
+       i_assert(stream->pos <= stream->buffer_size);
+Index: trixie/src/lib-compression/test-compression.c
+===================================================================
+--- trixie.orig/src/lib-compression/test-compression.c
++++ trixie/src/lib-compression/test-compression.c
+@@ -1052,6 +1052,11 @@ static const unsigned char lz4_chunk_cra
+        0x32, 0x01, 0xff, 0xff, 0x00, 0x30
+ };
+ 
++static const unsigned char lz4_chunk_eof_03[] = {
++      0x44, 0x6f, 0x76, 0x65, 0x63, 0x6f, 0x74, 0x2d, 0x4c, 0x5a, 0x34, 0x0d,
++      0x2a, 0x9b, 0xc5, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00,
++};
++
+ static void test_lz4_chunk_size(void)
+ {
+       const struct compression_handler *lz4;
+@@ -1088,6 +1093,18 @@ static void test_lz4_chunk_size(void)
+ 
+       i_stream_unref(&input);
+ 
++      file_input = test_istream_create_data(lz4_chunk_eof_03,
++                                            sizeof(lz4_chunk_eof_03));
++      input = lz4->create_istream(file_input);
++      i_stream_unref(&file_input);
++      i_stream_read(input);
++
++      /* no error, but no content either */
++      test_assert(input->eof);
++      test_assert(input->stream_errno == 0);
++
++      i_stream_unref(&input);
++
+       test_end();
+ }
+ 
diff --git a/patches/0001-lib-compression-istream-zlib-Use-container_of-macro.patch b/patches/0001-lib-compression-istream-zlib-Use-container_of-macro.patch
new file mode 100644 (file)
index 0000000..ca38266
--- /dev/null
@@ -0,0 +1,56 @@
+From 09df3b5b8b970439ab6b1b617b344a21cb2c07fe Mon Sep 17 00:00:00 2001
+From: Stephan Bosch <stephan.bosch@open-xchange.com>
+Date: Tue, 14 Nov 2023 16:57:45 +0100
+Subject: [PATCH] lib-compression: istream-zlib - Use container_of() macro
+
+---
+ src/lib-compression/istream-zlib.c | 12 ++++++++----
+ 1 file changed, 8 insertions(+), 4 deletions(-)
+
+diff --git a/src/lib-compression/istream-zlib.c b/src/lib-compression/istream-zlib.c
+index 93b3473071..0a1832a66d 100644
+--- a/src/lib-compression/istream-zlib.c
++++ b/src/lib-compression/istream-zlib.c
+@@ -40,7 +40,8 @@ static void i_stream_zlib_init(struct zlib_istream *zstream);
+ static void i_stream_zlib_close(struct iostream_private *stream,
+                               bool close_parent)
+ {
+-      struct zlib_istream *zstream = (struct zlib_istream *)stream;
++      struct zlib_istream *zstream =
++              container_of(stream, struct zlib_istream, istream.iostream);
+ 
+       if (!zstream->zs_closed) {
+               (void)inflateEnd(&zstream->zs);
+@@ -160,7 +161,8 @@ static int i_stream_zlib_read_trailer(struct zlib_istream *zstream)
+ 
+ static ssize_t i_stream_zlib_read(struct istream_private *stream)
+ {
+-      struct zlib_istream *zstream = (struct zlib_istream *)stream;
++      struct zlib_istream *zstream =
++              container_of(stream, struct zlib_istream, istream);
+       const unsigned char *data;
+       uoff_t high_offset;
+       size_t size, out_size;
+@@ -362,7 +364,8 @@ static void i_stream_zlib_reset(struct zlib_istream *zstream)
+ static void
+ i_stream_zlib_seek(struct istream_private *stream, uoff_t v_offset, bool mark)
+ {
+-      struct zlib_istream *zstream = (struct zlib_istream *) stream;
++      struct zlib_istream *zstream =
++              container_of(stream, struct zlib_istream, istream);
+ 
+       if (i_stream_nonseekable_try_seek(stream, v_offset))
+               return;
+@@ -378,7 +381,8 @@ i_stream_zlib_seek(struct istream_private *stream, uoff_t v_offset, bool mark)
+ 
+ static void i_stream_zlib_sync(struct istream_private *stream)
+ {
+-      struct zlib_istream *zstream = (struct zlib_istream *) stream;
++      struct zlib_istream *zstream =
++              container_of(stream, struct zlib_istream, istream);
+       const struct stat *st;
+ 
+       if (i_stream_stat(stream->parent, FALSE, &st) == 0) {
+-- 
+2.47.3
+
diff --git a/patches/0001-lib-compression-wrap-_read-in-for-loop-reindent-only.patch b/patches/0001-lib-compression-wrap-_read-in-for-loop-reindent-only.patch
new file mode 100644 (file)
index 0000000..59cbc99
--- /dev/null
@@ -0,0 +1,611 @@
+From aa8e26f7879895ff55bc247fb2594ffb3c34f4bc Mon Sep 17 00:00:00 2001
+From: Aki Tuomi <aki.tuomi@open-xchange.com>
+Date: Wed, 10 Jun 2026 11:53:39 +0000
+Subject: [PATCH 1/4] lib-compression: wrap *_read() in for(;;) loop (reindent
+ only)
+
+Prepare for the next commit: wrap the body of i_stream_lz4_read(),
+i_stream_bzlib_read(), and i_stream_zlib_read() in a for(;;) loop.
+No logic change; all paths still return on the first iteration.
+Separating the indent churn makes the actual fix easier to review.
+---
+ src/lib-compression/istream-bzlib.c | 158 ++++++++---------
+ src/lib-compression/istream-lz4.c   | 114 ++++++------
+ src/lib-compression/istream-zlib.c  | 260 ++++++++++++++--------------
+ 3 files changed, 269 insertions(+), 263 deletions(-)
+
+Index: trixie/src/lib-compression/istream-bzlib.c
+===================================================================
+--- trixie.orig/src/lib-compression/istream-bzlib.c
++++ trixie/src/lib-compression/istream-bzlib.c
+@@ -51,88 +51,90 @@ static ssize_t i_stream_bzlib_read(struc
+       size_t size, out_size;
+       int ret;
+ 
+-      high_offset = stream->istream.v_offset + (stream->pos - stream->skip);
+-      if (zstream->eof_offset == high_offset) {
+-              stream->istream.eof = TRUE;
+-              return -1;
+-      }
+-
+-      if (!zstream->marked) {
+-              if (!i_stream_try_alloc(stream, CHUNK_SIZE, &out_size))
+-                      return -2; /* buffer full */
+-      } else {
+-              /* try to avoid compressing, so we can quickly seek backwards */
+-              if (!i_stream_try_alloc_avoid_compress(stream, CHUNK_SIZE, &out_size))
+-                      return -2; /* buffer full */
+-      }
++      for (;;) {
++              high_offset = stream->istream.v_offset + (stream->pos - stream->skip);
++              if (zstream->eof_offset == high_offset) {
++                      stream->istream.eof = TRUE;
++                      return -1;
++              }
+ 
+-      if (i_stream_read_more(stream->parent, &data, &size) < 0) {
+-              if (stream->parent->stream_errno != 0) {
+-                      stream->istream.stream_errno =
+-                              stream->parent->stream_errno;
++              if (!zstream->marked) {
++                      if (!i_stream_try_alloc(stream, CHUNK_SIZE, &out_size))
++                              return -2; /* buffer full */
+               } else {
+-                      i_assert(stream->parent->eof);
+-                      bzlib_read_error(zstream, "unexpected EOF");
+-                      if (!zstream->hdr_read)
+-                              stream->istream.stream_errno = EINVAL;
+-                      else
+-                              stream->istream.stream_errno = EPIPE;
++                      /* try to avoid compressing, so we can quickly seek backwards */
++                      if (!i_stream_try_alloc_avoid_compress(stream, CHUNK_SIZE, &out_size))
++                              return -2; /* buffer full */
++              }
++
++              if (i_stream_read_more(stream->parent, &data, &size) < 0) {
++                      if (stream->parent->stream_errno != 0) {
++                              stream->istream.stream_errno =
++                                      stream->parent->stream_errno;
++                      } else {
++                              i_assert(stream->parent->eof);
++                              bzlib_read_error(zstream, "unexpected EOF");
++                              if (!zstream->hdr_read)
++                                      stream->istream.stream_errno = EINVAL;
++                              else
++                                      stream->istream.stream_errno = EPIPE;
++                      }
++                      return -1;
++              }
++              if (size == 0) {
++                      /* no more input */
++                      i_assert(!stream->istream.blocking);
++                      return 0;
+               }
+-              return -1;
+-      }
+-      if (size == 0) {
+-              /* no more input */
+-              i_assert(!stream->istream.blocking);
+-              return 0;
+-      }
+ 
+-      zstream->zs.next_in = (char *)data;
+-      zstream->zs.avail_in = size;
++              zstream->zs.next_in = (char *)data;
++              zstream->zs.avail_in = size;
+ 
+-      zstream->zs.next_out = (char *)stream->w_buffer + stream->pos;
+-      zstream->zs.avail_out = out_size;
+-      ret = BZ2_bzDecompress(&zstream->zs);
+-      zstream->hdr_read = TRUE;
+-
+-      out_size -= zstream->zs.avail_out;
+-      stream->pos += out_size;
+-
+-      i_stream_skip(stream->parent, size - zstream->zs.avail_in);
+-
+-      switch (ret) {
+-      case BZ_OK:
+-              break;
+-      case BZ_PARAM_ERROR:
+-              i_unreached();
+-      case BZ_DATA_ERROR:
+-              bzlib_read_error(zstream, "corrupted data");
+-              stream->istream.stream_errno = EINVAL;
+-              return -1;
+-      case BZ_DATA_ERROR_MAGIC:
+-              bzlib_read_error(zstream,
+-                      "wrong magic in header (not bz2 file?)");
+-              stream->istream.stream_errno = EINVAL;
+-              return -1;
+-      case BZ_MEM_ERROR:
+-              i_fatal_status(FATAL_OUTOFMEM, "bzlib.read(%s): Out of memory",
+-                             i_stream_get_name(&stream->istream));
+-      case BZ_STREAM_END:
+-              zstream->eof_offset = stream->istream.v_offset +
+-                      (stream->pos - stream->skip);
+-              stream->cached_stream_size = zstream->eof_offset;
+-              if (out_size == 0) {
+-                      stream->istream.eof = TRUE;
++              zstream->zs.next_out = (char *)stream->w_buffer + stream->pos;
++              zstream->zs.avail_out = out_size;
++              ret = BZ2_bzDecompress(&zstream->zs);
++              zstream->hdr_read = TRUE;
++
++              out_size -= zstream->zs.avail_out;
++              stream->pos += out_size;
++
++              i_stream_skip(stream->parent, size - zstream->zs.avail_in);
++
++              switch (ret) {
++              case BZ_OK:
++                      break;
++              case BZ_PARAM_ERROR:
++                      i_unreached();
++              case BZ_DATA_ERROR:
++                      bzlib_read_error(zstream, "corrupted data");
++                      stream->istream.stream_errno = EINVAL;
+                       return -1;
++              case BZ_DATA_ERROR_MAGIC:
++                      bzlib_read_error(zstream,
++                              "wrong magic in header (not bz2 file?)");
++                      stream->istream.stream_errno = EINVAL;
++                      return -1;
++              case BZ_MEM_ERROR:
++                      i_fatal_status(FATAL_OUTOFMEM, "bzlib.read(%s): Out of memory",
++                                     i_stream_get_name(&stream->istream));
++              case BZ_STREAM_END:
++                      zstream->eof_offset = stream->istream.v_offset +
++                              (stream->pos - stream->skip);
++                      stream->cached_stream_size = zstream->eof_offset;
++                      if (out_size == 0) {
++                              stream->istream.eof = TRUE;
++                              return -1;
++                      }
++                      break;
++              default:
++                      i_fatal("BZ2_bzDecompress() failed with %d", ret);
+               }
+-              break;
+-      default:
+-              i_fatal("BZ2_bzDecompress() failed with %d", ret);
+-      }
+-      if (out_size == 0) {
+-              /* read more input */
+-              return i_stream_bzlib_read(stream);
++              if (out_size == 0) {
++                      /* read more input */
++                      return i_stream_bzlib_read(stream);
++              }
++              return out_size;
+       }
+-      return out_size;
+ }
+ 
+ static void i_stream_bzlib_init(struct bzlib_istream *zstream)
+Index: trixie/src/lib-compression/istream-lz4.c
+===================================================================
+--- trixie.orig/src/lib-compression/istream-lz4.c
++++ trixie/src/lib-compression/istream-lz4.c
+@@ -149,67 +149,69 @@ static ssize_t i_stream_lz4_read(struct
+               zstream->header_read = TRUE;
+       }
+ 
+-      if (zstream->chunk_left == 0) {
+-              while ((ret = i_stream_lz4_read_chunk_header(zstream)) == 0) {
+-                      if (!stream->istream.blocking)
+-                              return 0;
++      for (;;) {
++              if (zstream->chunk_left == 0) {
++                      while ((ret = i_stream_lz4_read_chunk_header(zstream)) == 0) {
++                              if (!stream->istream.blocking)
++                                      return 0;
++                      }
++                      if (ret < 0)
++                              return ret;
+               }
+-              if (ret < 0)
+-                      return ret;
+-      }
+ 
+-      /* read the whole compressed chunk into memory */
+-      while (zstream->chunk_left > 0 &&
+-             (ret = i_stream_read_more(zstream->istream.parent, &data, &size)) > 0) {
+-              if (size > zstream->chunk_left)
+-                      size = zstream->chunk_left;
+-              buffer_append(zstream->chunk_buf, data, size);
+-              i_stream_skip(zstream->istream.parent, size);
+-              zstream->chunk_left -= size;
+-      }
+-      if (zstream->chunk_left > 0) {
+-              if (ret == -1 && zstream->istream.parent->stream_errno == 0) {
+-                      lz4_read_error(zstream, "truncated lz4 chunk");
+-                      stream->istream.stream_errno = EPIPE;
+-                      return -1;
++              /* read the whole compressed chunk into memory */
++              while (zstream->chunk_left > 0 &&
++                     (ret = i_stream_read_more(zstream->istream.parent, &data, &size)) > 0) {
++                      if (size > zstream->chunk_left)
++                              size = zstream->chunk_left;
++                      buffer_append(zstream->chunk_buf, data, size);
++                      i_stream_skip(zstream->istream.parent, size);
++                      zstream->chunk_left -= size;
+               }
+-              zstream->istream.istream.stream_errno =
+-                      zstream->istream.parent->stream_errno;
+-              i_assert(ret != 0 || !stream->istream.blocking);
+-              return ret;
+-      }
+-      /* if we already have max_buffer_size amount of data, fail here */
+-      if (stream->pos - stream->skip >= i_stream_get_max_buffer_size(&stream->istream))
+-              return -2;
+-      if (i_stream_get_data_size(zstream->istream.parent) > 0) {
+-              /* Parent stream was only partially consumed. Set the stream's
+-                 IO as pending to avoid hangs. */
+-              i_stream_set_input_pending(&zstream->istream.istream, TRUE);
+-      }
+-      /* allocate enough space for the old data and the new
+-         decompressed chunk. we don't know the original compressed size,
+-         so just allocate the max amount of memory. */
+-      void *dest = i_stream_alloc(stream, zstream->max_uncompressed_chunk_size);
+-      ret = LZ4_decompress_safe(zstream->chunk_buf->data, dest,
+-                                zstream->chunk_buf->used,
+-                                zstream->max_uncompressed_chunk_size);
+-      i_assert(ret <= (int)zstream->max_uncompressed_chunk_size);
+-      if (ret < 0) {
+-              lz4_read_error(zstream, "corrupted lz4 chunk");
+-              stream->istream.stream_errno = EINVAL;
+-              return -1;
+-      } else if (ret == 0)
+-              return i_stream_lz4_read(stream);
+-      i_assert(ret > 0);
+-      stream->pos += ret;
+-      i_assert(stream->pos <= stream->buffer_size);
++              if (zstream->chunk_left > 0) {
++                      if (ret == -1 && zstream->istream.parent->stream_errno == 0) {
++                              lz4_read_error(zstream, "truncated lz4 chunk");
++                              stream->istream.stream_errno = EPIPE;
++                              return -1;
++                      }
++                      zstream->istream.istream.stream_errno =
++                              zstream->istream.parent->stream_errno;
++                      i_assert(ret != 0 || !stream->istream.blocking);
++                      return ret;
++              }
++              /* if we already have max_buffer_size amount of data, fail here */
++              if (stream->pos - stream->skip >= i_stream_get_max_buffer_size(&stream->istream))
++                      return -2;
++              if (i_stream_get_data_size(zstream->istream.parent) > 0) {
++                      /* Parent stream was only partially consumed. Set the stream's
++                         IO as pending to avoid hangs. */
++                      i_stream_set_input_pending(&zstream->istream.istream, TRUE);
++              }
++              /* allocate enough space for the old data and the new
++                 decompressed chunk. we don't know the original compressed size,
++                 so just allocate the max amount of memory. */
++              void *dest = i_stream_alloc(stream, zstream->max_uncompressed_chunk_size);
++              ret = LZ4_decompress_safe(zstream->chunk_buf->data, dest,
++                                        zstream->chunk_buf->used,
++                                        zstream->max_uncompressed_chunk_size);
++              i_assert(ret <= (int)zstream->max_uncompressed_chunk_size);
++              if (ret < 0) {
++                      lz4_read_error(zstream, "corrupted lz4 chunk");
++                      stream->istream.stream_errno = EINVAL;
++                      return -1;
++              } else if (ret == 0)
++                      return i_stream_lz4_read(stream);
++              i_assert(ret > 0);
++              stream->pos += ret;
++              i_assert(stream->pos <= stream->buffer_size);
+ 
+-      /* we are going to get next chunk after this, so reset here
+-         so we can reuse the chunk buf for reading next buffer prefix */
+-      if (zstream->chunk_left == 0)
+-              buffer_set_used_size(zstream->chunk_buf, 0);
++              /* we are going to get next chunk after this, so reset here
++                 so we can reuse the chunk buf for reading next buffer prefix */
++              if (zstream->chunk_left == 0)
++                      buffer_set_used_size(zstream->chunk_buf, 0);
+ 
+-      return ret;
++              return ret;
++      }
+ }
+ 
+ static void i_stream_lz4_reset(struct lz4_istream *zstream)
+Index: trixie/src/lib-compression/istream-zlib.c
+===================================================================
+--- trixie.orig/src/lib-compression/istream-zlib.c
++++ trixie/src/lib-compression/istream-zlib.c
+@@ -168,155 +168,157 @@ static ssize_t i_stream_zlib_read(struct
+       size_t size, out_size;
+       int ret;
+ 
+-      high_offset = stream->istream.v_offset + (stream->pos - stream->skip);
+-      if (zstream->eof_offset == high_offset) {
+-              /* zlib library returned EOF. */
+-              if (!zstream->gz) {
+-                      /* deflate - ignore if there's still more data */
+-                      stream->istream.eof = TRUE;
+-                      return -1;
++      for (;;) {
++              high_offset = stream->istream.v_offset + (stream->pos - stream->skip);
++              if (zstream->eof_offset == high_offset) {
++                      /* zlib library returned EOF. */
++                      if (!zstream->gz) {
++                              /* deflate - ignore if there's still more data */
++                              stream->istream.eof = TRUE;
++                              return -1;
++                      }
++                      /* gz format - read the trailer */
++                      if (!zstream->trailer_read) {
++                              do {
++                                      ret = i_stream_zlib_read_trailer(zstream);
++                              } while (ret == 0 && stream->istream.blocking);
++                              if (ret <= 0)
++                                      return ret;
++                      }
++                      /* See if there's another concatenated gz stream. */
++                      if (i_stream_read_eof(stream->parent)) {
++                              /* EOF or error */
++                              stream->istream.stream_errno =
++                                      stream->parent->stream_errno;
++                              stream->istream.eof = TRUE;
++                              return -1;
++                      }
++                      /* Multiple gz streams concatenated together */
++                      zstream->starting_concatenated_output = TRUE;
++              }
++              if (zstream->starting_concatenated_output) {
++                      /* make sure there actually is something in parent stream.
++                         we don't want to reset the stream unless we actually see
++                         some concatenated output. */
++                      ret = i_stream_read_more(stream->parent, &data, &size);
++                      if (ret <= 0) {
++                              if (ret == 0)
++                                      return 0;
++                              if (stream->parent->stream_errno != 0) {
++                                      stream->istream.stream_errno =
++                                              stream->parent->stream_errno;
++                              }
++                              stream->istream.eof = TRUE;
++                              return -1;
++                      }
++
++                      /* gzip file with concatenated content */
++                      stream->cached_stream_size = UOFF_T_MAX;
++                      zstream->eof_offset = UOFF_T_MAX;
++                      zstream->header_read = FALSE;
++                      zstream->trailer_read = FALSE;
++                      zstream->crc32 = 0;
++                      zstream->starting_concatenated_output = FALSE;
++
++                      (void)inflateEnd(&zstream->zs);
++                      i_stream_zlib_init(zstream);
+               }
+-              /* gz format - read the trailer */
+-              if (!zstream->trailer_read) {
++
++              if (!zstream->header_read) {
+                       do {
+-                              ret = i_stream_zlib_read_trailer(zstream);
++                              ret = i_stream_zlib_read_header(stream);
+                       } while (ret == 0 && stream->istream.blocking);
+                       if (ret <= 0)
+                               return ret;
++                      zstream->header_read = TRUE;
+               }
+-              /* See if there's another concatenated gz stream. */
+-              if (i_stream_read_eof(stream->parent)) {
+-                      /* EOF or error */
+-                      stream->istream.stream_errno =
+-                              stream->parent->stream_errno;
+-                      stream->istream.eof = TRUE;
+-                      return -1;
++
++              if (!zstream->marked) {
++                      if (!i_stream_try_alloc(stream, CHUNK_SIZE, &out_size))
++                              return -2; /* buffer full */
++              } else {
++                      /* try to avoid compressing, so we can quickly seek backwards */
++                      if (!i_stream_try_alloc_avoid_compress(stream, CHUNK_SIZE, &out_size))
++                              return -2; /* buffer full */
+               }
+-              /* Multiple gz streams concatenated together */
+-              zstream->starting_concatenated_output = TRUE;
+-      }
+-      if (zstream->starting_concatenated_output) {
+-              /* make sure there actually is something in parent stream.
+-                 we don't want to reset the stream unless we actually see
+-                 some concatenated output. */
+-              ret = i_stream_read_more(stream->parent, &data, &size);
+-              if (ret <= 0) {
+-                      if (ret == 0)
+-                              return 0;
++
++              if (i_stream_read_more(stream->parent, &data, &size) < 0) {
+                       if (stream->parent->stream_errno != 0) {
+                               stream->istream.stream_errno =
+                                       stream->parent->stream_errno;
++                      } else {
++                              i_assert(stream->parent->eof);
++                              zlib_read_error(zstream, "unexpected EOF");
++                              stream->istream.stream_errno = EPIPE;
+                       }
+-                      stream->istream.eof = TRUE;
+                       return -1;
+               }
+-
+-              /* gzip file with concatenated content */
+-              stream->cached_stream_size = UOFF_T_MAX;
+-              zstream->eof_offset = UOFF_T_MAX;
+-              zstream->header_read = FALSE;
+-              zstream->trailer_read = FALSE;
+-              zstream->crc32 = 0;
+-              zstream->starting_concatenated_output = FALSE;
+-
+-              (void)inflateEnd(&zstream->zs);
+-              i_stream_zlib_init(zstream);
+-      }
+-
+-      if (!zstream->header_read) {
+-              do {
+-                      ret = i_stream_zlib_read_header(stream);
+-              } while (ret == 0 && stream->istream.blocking);
+-              if (ret <= 0)
+-                      return ret;
+-              zstream->header_read = TRUE;
+-      }
+-
+-      if (!zstream->marked) {
+-              if (!i_stream_try_alloc(stream, CHUNK_SIZE, &out_size))
+-                      return -2; /* buffer full */
+-      } else {
+-              /* try to avoid compressing, so we can quickly seek backwards */
+-              if (!i_stream_try_alloc_avoid_compress(stream, CHUNK_SIZE, &out_size))
+-                      return -2; /* buffer full */
+-      }
+-
+-      if (i_stream_read_more(stream->parent, &data, &size) < 0) {
+-              if (stream->parent->stream_errno != 0) {
+-                      stream->istream.stream_errno =
+-                              stream->parent->stream_errno;
+-              } else {
+-                      i_assert(stream->parent->eof);
+-                      zlib_read_error(zstream, "unexpected EOF");
+-                      stream->istream.stream_errno = EPIPE;
+-              }
+-              return -1;
+-      }
+-      if (size == 0) {
+-              /* no more input */
+-              i_assert(!stream->istream.blocking);
+-              return 0;
+-      }
+-
+-      zstream->zs.next_in = (void *)data;
+-      zstream->zs.avail_in = size;
+-
+-      zstream->zs.next_out = stream->w_buffer + stream->pos;
+-      zstream->zs.avail_out = out_size;
+-      ret = inflate(&zstream->zs, Z_SYNC_FLUSH);
+-
+-      out_size -= zstream->zs.avail_out;
+-      /* CRC32 is only needed for GZ trailer. */
+-      if (zstream->gz)
+-              zstream->crc32 = crc32_data_more(zstream->crc32,
+-                                               stream->w_buffer + stream->pos,
+-                                               out_size);
+-      stream->pos += out_size;
+-
+-      size_t bytes_consumed = size - zstream->zs.avail_in;
+-      i_stream_skip(stream->parent, bytes_consumed);
+-      if (i_stream_get_data_size(stream->parent) > 0 &&
+-          (bytes_consumed > 0 || out_size > 0)) {
+-              /* Parent stream was only partially consumed. Set the stream's
+-                 IO as pending to avoid hangs. */
+-              i_stream_set_input_pending(&stream->istream, TRUE);
+-      }
+-
+-      switch (ret) {
+-      case Z_OK:
+-              break;
+-      case Z_NEED_DICT:
+-              zlib_read_error(zstream, "can't read file without dict");
+-              stream->istream.stream_errno = EIO;
+-              return -1;
+-      case Z_DATA_ERROR:
+-              zlib_read_error(zstream, "corrupted data");
+-              stream->istream.stream_errno = EINVAL;
+-              return -1;
+-      case Z_MEM_ERROR:
+-              i_fatal_status(FATAL_OUTOFMEM, "zlib.read(%s): Out of memory",
+-                             i_stream_get_name(&stream->istream));
+-      case Z_STREAM_END:
+-              zstream->eof_offset = stream->istream.v_offset +
+-                      (stream->pos - stream->skip);
+-              stream->cached_stream_size = zstream->eof_offset;
+-              zstream->zs.avail_in = 0;
+-
+-              if (!zstream->trailer_read) {
+-                      /* try to read and verify the trailer, we might not
+-                         be called again. */
+-                      if (i_stream_zlib_read_trailer(zstream) < 0)
+-                              return -1;
++              if (size == 0) {
++                      /* no more input */
++                      i_assert(!stream->istream.blocking);
++                      return 0;
++              }
++
++              zstream->zs.next_in = (void *)data;
++              zstream->zs.avail_in = size;
++
++              zstream->zs.next_out = stream->w_buffer + stream->pos;
++              zstream->zs.avail_out = out_size;
++              ret = inflate(&zstream->zs, Z_SYNC_FLUSH);
++
++              out_size -= zstream->zs.avail_out;
++              /* CRC32 is only needed for GZ trailer. */
++              if (zstream->gz)
++                      zstream->crc32 = crc32_data_more(zstream->crc32,
++                                                       stream->w_buffer + stream->pos,
++                                                       out_size);
++              stream->pos += out_size;
++
++              size_t bytes_consumed = size - zstream->zs.avail_in;
++              i_stream_skip(stream->parent, bytes_consumed);
++              if (i_stream_get_data_size(stream->parent) > 0 &&
++                  (bytes_consumed > 0 || out_size > 0)) {
++                      /* Parent stream was only partially consumed. Set the stream's
++                         IO as pending to avoid hangs. */
++                      i_stream_set_input_pending(&stream->istream, TRUE);
++              }
++
++              switch (ret) {
++              case Z_OK:
++                      break;
++              case Z_NEED_DICT:
++                      zlib_read_error(zstream, "can't read file without dict");
++                      stream->istream.stream_errno = EIO;
++                      return -1;
++              case Z_DATA_ERROR:
++                      zlib_read_error(zstream, "corrupted data");
++                      stream->istream.stream_errno = EINVAL;
++                      return -1;
++              case Z_MEM_ERROR:
++                      i_fatal_status(FATAL_OUTOFMEM, "zlib.read(%s): Out of memory",
++                                     i_stream_get_name(&stream->istream));
++              case Z_STREAM_END:
++                      zstream->eof_offset = stream->istream.v_offset +
++                              (stream->pos - stream->skip);
++                      stream->cached_stream_size = zstream->eof_offset;
++                      zstream->zs.avail_in = 0;
++
++                      if (!zstream->trailer_read) {
++                              /* try to read and verify the trailer, we might not
++                                 be called again. */
++                              if (i_stream_zlib_read_trailer(zstream) < 0)
++                                      return -1;
++                      }
++                      break;
++              default:
++                      i_fatal("inflate() failed with %d", ret);
++              }
++              if (out_size == 0) {
++                      /* read more input */
++                      return i_stream_zlib_read(stream);
+               }
+-              break;
+-      default:
+-              i_fatal("inflate() failed with %d", ret);
+-      }
+-      if (out_size == 0) {
+-              /* read more input */
+-              return i_stream_zlib_read(stream);
++              return out_size;
+       }
+-      return out_size;
+ }
+ 
+ static void i_stream_zlib_init(struct zlib_istream *zstream)
diff --git a/patches/0001-lib-i_close_fd-Document-that-it-preserves-errno.patch b/patches/0001-lib-i_close_fd-Document-that-it-preserves-errno.patch
new file mode 100644 (file)
index 0000000..21c07ce
--- /dev/null
@@ -0,0 +1,25 @@
+From 5e72915a0d803154ab50958364ff4ce78ea146da Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Wed, 3 Jun 2026 18:04:42 +0300
+Subject: [PATCH 1/5] lib: i_close_fd*() - Document that it preserves errno
+
+---
+ src/lib/fd-util.h | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+diff --git a/src/lib/fd-util.h b/src/lib/fd-util.h
+index 54bdd63160..3f46892759 100644
+--- a/src/lib/fd-util.h
++++ b/src/lib/fd-util.h
+@@ -17,7 +17,7 @@ void fd_close_maybe_stdio(int *fd_in, int *fd_out);
+ 
+ /* Close the fd and set it to -1. This assert-crashes if fd == 0, and is a
+    no-op if fd == -1. Normally fd == 0 would happen only if an uninitialized
+-   fd is attempted to be closed, which is a bug. */
++   fd is attempted to be closed, which is a bug. Preserves original errno. */
+ void i_close_fd_path(int *fd, const char *path, const char *arg,
+                    const char *func, const char *file, int line);
+ #define i_close_fd_path(fd, path) i_close_fd_path((fd), (path), #fd, __func__, __FILE__, __LINE__)
+-- 
+2.39.5
+
diff --git a/patches/0001-lib-imap-imap-match-Fix-excessive-CPU-usage-caused-b.patch b/patches/0001-lib-imap-imap-match-Fix-excessive-CPU-usage-caused-b.patch
new file mode 100644 (file)
index 0000000..d492e49
--- /dev/null
@@ -0,0 +1,524 @@
+From 16ff02fd5635a3b6583a276377a8b6880047d601 Mon Sep 17 00:00:00 2001
+From: Noah Meyerhans <noahm@debian.org>
+Date: Thu, 10 Sep 2026 09:59:18 -0400
+Subject: [PATCH] lib-imap: imap-match - Fix excessive CPU usage caused by
+ backtracking
+MIME-Version: 1.0
+Content-Type: text/plain; charset=UTF-8
+Content-Transfer-Encoding: 8bit
+
+Backport the NFA/non-backtracking algorithm from 2684624… while retaining the
+byte-oriented matcher semantics of 2.4.1.
+
+Original commit message:
+
+Replace the recursive backtracking matcher with a Thompson-style NFA
+simulation over grapheme clusters.
+
+Each grapheme cluster of the compressed pattern becomes one state:
+LITERAL, PERCENT (consume any number of non-separator clusters) or STAR
+(consume any number of clusters including separators).  A virtual
+ACCEPT position sits at index n_states.  Simulation tracks the set of
+active positions in a bitmap.  Epsilon-closure (skipping a PERCENT or
+STAR without consuming) is a single forward pass because the NFA is
+linear: each state can only epsilon-skip to i+1.
+
+The resulting match is O(n_data * n_pattern) regardless of pattern
+shape, with no recursion and no backtracking, so there is no way for
+a malicious pattern or mailbox name to trigger exponential CPU,
+excessive stack depth, or unbounded memory.
+
+IMAP_MATCH_YES / NO / CHILDREN / PARENT semantics are preserved:
+
+- YES:      ACCEPT reachable after consuming all data.
+- PARENT:   ACCEPT was active at some point while the next data
+         grapheme cluster was the separator.
+- CHILDREN: some active non-ACCEPT state remains after consuming all
+         data, and either the data ends with a separator or an
+         active state can still consume a separator (precomputed
+         as sep_accept[]).
+
+Inboxcase handling (case-insensitive comparison for the INBOX prefix
+of data) and grapheme-cluster comparison are unchanged - the existing
+match_gc logic is reused inline as literal_matches().
+pattern_compress() and pattern_is_inboxcase() are unchanged.
+---
+ src/lib-imap/imap-match.c      | 307 ++++++++++++++++++++-------------
+ src/lib-imap/test-imap-match.c |  52 ++++++
+ 2 files changed, 239 insertions(+), 120 deletions(-)
+
+diff --git a/src/lib-imap/imap-match.c b/src/lib-imap/imap-match.c
+index 8603e004fb..fec5a3c242 100644
+--- a/src/lib-imap/imap-match.c
++++ b/src/lib-imap/imap-match.c
+@@ -9,9 +9,44 @@
+ 
+ #include <ctype.h>
+ 
++/* Pattern matching is implemented as a Thompson-style NFA simulation.
++   Each byte in the compressed pattern becomes one NFA state:
++
++     LITERAL - must consume a matching byte, with inboxcase fallback
++     PERCENT - may consume any number of non-separator bytes
++     STAR    - may consume any number of bytes, including separators
++
++   A virtual ACCEPT position sits at index n_states.  Simulation tracks the
++   set of active positions in boolean arrays.  Epsilon transitions (skipping
++   a PERCENT or STAR without consuming) are applied as a single forward pass,
++   because the NFA is linear: each state can only epsilon-skip to i+1.
++
++   Complexity is O(n_data * n_pattern), regardless of wildcard count or
++   pattern shape, so there is no recursive backtracking and no way for a
++   malicious pattern or mailbox name to trigger exponential CPU usage.
++
++   This 2.4.1 backport keeps the historical byte-oriented matching model.
++   The upstream fix operates on grapheme clusters, but backporting that would
++   require additional Unicode matching changes outside the minimal ReDoS fix. */
++
++enum imap_match_nfa_type {
++      IMAP_MATCH_NFA_LITERAL = 0,
++      IMAP_MATCH_NFA_PERCENT,
++      IMAP_MATCH_NFA_STAR
++};
++
++struct imap_match_nfa_state {
++      enum imap_match_nfa_type type;
++      bool sep_accept;
++      unsigned char ch;
++};
++
+ struct imap_match_pattern {
+       const char *pattern;
+       bool inboxcase;
++
++      unsigned int n_states;
++      struct imap_match_nfa_state *states;
+ };
+ 
+ struct imap_match_glob {
+@@ -23,13 +58,6 @@ struct imap_match_glob {
+       char patterns_data[FLEXIBLE_ARRAY_MEMBER];
+ };
+ 
+-struct imap_match_context {
+-      const char *inboxcase_end;
+-
+-      char sep;
+-      bool inboxcase;
+-};
+-
+ /* name of "INBOX" - must not have repeated substrings */
+ static const char inbox[] = "INBOX";
+ #define INBOXLEN (sizeof(inbox) - 1)
+@@ -108,6 +136,48 @@ static bool pattern_is_inboxcase(const char *pattern, char separator)
+       return TRUE;
+ }
+ 
++static void
++imap_match_compile(pool_t pool, struct imap_match_pattern *pat, char sep)
++{
++      unsigned int i;
++
++      pat->n_states = strlen(pat->pattern);
++      pat->states = pat->n_states == 0 ? NULL :
++              p_new(pool, struct imap_match_nfa_state, pat->n_states);
++
++      for (i = 0; i < pat->n_states; i++) {
++              struct imap_match_nfa_state *state = &pat->states[i];
++              unsigned char ch = (unsigned char)pat->pattern[i];
++
++              if (ch == '%')
++                      state->type = IMAP_MATCH_NFA_PERCENT;
++              else if (ch == '*')
++                      state->type = IMAP_MATCH_NFA_STAR;
++              else {
++                      state->type = IMAP_MATCH_NFA_LITERAL;
++                      state->ch = ch;
++              }
++      }
++
++      for (i = pat->n_states; i > 0; i--) {
++              unsigned int idx = i - 1;
++              const struct imap_match_nfa_state *state = &pat->states[idx];
++              bool consume_sep =
++                      state->type == IMAP_MATCH_NFA_STAR ||
++                      (state->type == IMAP_MATCH_NFA_LITERAL &&
++                       state->ch == (unsigned char)sep);
++              bool eps_skippable =
++                      state->type == IMAP_MATCH_NFA_PERCENT ||
++                      state->type == IMAP_MATCH_NFA_STAR;
++              bool next_sep_accept =
++                      idx + 1 < pat->n_states ?
++                      pat->states[idx + 1].sep_accept : FALSE;
++
++              pat->states[idx].sep_accept =
++                      consume_sep || (eps_skippable && next_sep_accept);
++      }
++}
++
+ static struct imap_match_glob *
+ imap_match_init_multiple_real(pool_t pool, const char *const *patterns,
+                             bool inboxcase, char separator)
+@@ -138,7 +208,7 @@ imap_match_init_multiple_real(pool_t pool, const char *const *patterns,
+       glob->pool = pool;
+       glob->sep = separator;
+ 
+-      /* copy pattern strings to our allocated memory */
++      /* copy pattern strings to our allocated memory and compile NFAs */
+       for (i = 0, pos = 0; i < patterns_count; i++) {
+               len = strlen(match_patterns[i].pattern) + 1;
+               i_assert(pos + len <= patterns_data_len);
+@@ -148,6 +218,8 @@ imap_match_init_multiple_real(pool_t pool, const char *const *patterns,
+                      match_patterns[i].pattern, len);
+               match_patterns[i].pattern = glob->patterns_data + pos;
+               pos += len;
++
++              imap_match_compile(pool, &match_patterns[i], separator);
+       }
+       glob->patterns = match_patterns;
+       return glob;
+@@ -172,8 +244,15 @@ imap_match_init_multiple(pool_t pool, const char *const *patterns,
+ 
+ void imap_match_deinit(struct imap_match_glob **glob)
+ {
++      struct imap_match_pattern *p;
++
+       if (glob == NULL || *glob == NULL)
+               return;
++
++      for (p = (*glob)->patterns; p->pattern != NULL; p++) {
++              if (p->states != NULL)
++                      p_free((*glob)->pool, p->states);
++      }
+       p_free((*glob)->pool, (*glob)->patterns);
+       p_free((*glob)->pool, *glob);
+       *glob = NULL;
+@@ -230,148 +309,136 @@ bool imap_match_globs_equal(const struct imap_match_glob *glob1,
+       return p1->pattern == p2->pattern;
+ }
+ 
+-#define CMP_CUR_CHR(ctx, data, pattern) \
+-      (*(data) == *(pattern) || \
+-       (i_toupper(*(data)) == i_toupper(*(pattern)) && \
+-       (data) < (ctx)->inboxcase_end))
++static bool
++literal_matches(const struct imap_match_nfa_state *state,
++              unsigned char data_ch, bool inboxcase_pos)
++{
++      if (state->ch == data_ch)
++              return TRUE;
++      return inboxcase_pos &&
++              i_toupper(data_ch) == i_toupper(state->ch);
++}
+ 
+-static enum imap_match_result
+-match_sub(struct imap_match_context *ctx, const char **data_p,
+-        const char **pattern_p)
++static void
++nfa_eps_close(const struct imap_match_pattern *pat, bool *bits)
+ {
+-      enum imap_match_result ret, match;
+       unsigned int i;
+-      const char *data = *data_p, *pattern = *pattern_p;
+-
+-      /* match all non-wildcards */
+-      i = 0;
+-      while (pattern[i] != '\0' && pattern[i] != '*' && pattern[i] != '%') {
+-              if (!CMP_CUR_CHR(ctx, data+i, pattern+i)) {
+-                      if (data[i] != '\0')
+-                              return IMAP_MATCH_NO;
+-                      if (pattern[i] == ctx->sep)
+-                              return IMAP_MATCH_CHILDREN;
+-                      if (i > 0 && pattern[i-1] == ctx->sep) {
+-                              /* data="foo/" pattern = "foo/bar/%" */
+-                              return IMAP_MATCH_CHILDREN;
+-                      }
+-                      return IMAP_MATCH_NO;
+-              }
+-              i++;
+-      }
+-      data += i;
+-      pattern += i;
+ 
+-      if (*data == '\0' && *data_p != data && data[-1] == ctx->sep &&
+-          *pattern != '\0') {
+-              /* data="/" pattern="/%..." */
+-              match = IMAP_MATCH_CHILDREN;
+-      } else {
+-              match = IMAP_MATCH_NO;
++      for (i = 0; i < pat->n_states; i++) {
++              if (!bits[i])
++                      continue;
++              if (pat->states[i].type == IMAP_MATCH_NFA_PERCENT ||
++                  pat->states[i].type == IMAP_MATCH_NFA_STAR)
++                      bits[i + 1] = TRUE;
+       }
+-      while (*pattern == '%') {
+-              pattern++;
+-
+-              if (*pattern == '\0') {
+-                      /* match, if this is the last hierarchy */
+-                      while (*data != '\0' && *data != ctx->sep)
+-                              data++;
+-                      break;
+-              }
+-
+-              /* skip over this hierarchy */
+-              while (*data != '\0') {
+-                      if (CMP_CUR_CHR(ctx, data, pattern)) {
+-                              ret = match_sub(ctx, &data, &pattern);
+-                              if (ret == IMAP_MATCH_YES)
+-                                      break;
+-
+-                              match |= ret;
+-                      }
+-
+-                      if (*data == ctx->sep)
+-                              break;
++}
+ 
+-                      data++;
+-              }
++static enum imap_match_result
++imap_match_pattern_run(const struct imap_match_pattern *pat,
++                     const char *data, char sep, bool inboxcase_pattern)
++{
++      const char *inboxcase_end = data;
++      unsigned int n_bits = pat->n_states + 1;
++      enum imap_match_result result = IMAP_MATCH_NO;
++      bool parent_flag = FALSE;
++      bool data_ends_with_sep = FALSE;
++      bool *cur, *next;
++      const unsigned char *p;
++
++      if (inboxcase_pattern &&
++          strncasecmp(data, inbox, INBOXLEN) == 0 &&
++          (data[INBOXLEN] == '\0' || data[INBOXLEN] == sep)) {
++              inboxcase_end += INBOXLEN;
+       }
+ 
+-      if (*pattern != '*') {
+-              if (*data == '\0' && *pattern != '\0') {
+-                      if (*pattern == ctx->sep)
+-                              match |= IMAP_MATCH_CHILDREN;
+-                      return match;
+-              }
++      cur = t_new(bool, n_bits);
++      next = t_new(bool, n_bits);
++      memset(cur, 0, n_bits * sizeof(*cur));
++      memset(next, 0, n_bits * sizeof(*next));
+ 
+-              if (*data != '\0') {
+-                      if (*pattern == '\0' && *data == ctx->sep)
+-                              match |= IMAP_MATCH_PARENT;
+-                      return match;
+-              }
+-      }
++      cur[0] = TRUE;
++      nfa_eps_close(pat, cur);
+ 
+-      *data_p = data;
+-      *pattern_p = pattern;
+-      return IMAP_MATCH_YES;
+-}
++      for (p = (const unsigned char *)data; *p != '\0'; p++) {
++              unsigned int i;
++              unsigned char ch = *p;
++              bool ch_is_sep = ch == (unsigned char)sep;
++              bool inboxcase_pos = (const char *)p < inboxcase_end;
+ 
+-static enum imap_match_result
+-imap_match_pattern(struct imap_match_context *ctx,
+-                 const char *data, const char *pattern)
+-{
+-      enum imap_match_result ret, match;
++              if (ch_is_sep && cur[pat->n_states])
++                      parent_flag = TRUE;
+ 
+-      ctx->inboxcase_end = data;
+-      if (ctx->inboxcase && strncasecmp(data, inbox, INBOXLEN) == 0 &&
+-          (data[INBOXLEN] == '\0' || data[INBOXLEN] == ctx->sep)) {
+-              /* data begins with INBOX/, use case-insensitive comparison
+-                 for it */
+-              ctx->inboxcase_end += INBOXLEN;
+-      }
++              memset(next, 0, n_bits * sizeof(*next));
++              for (i = 0; i < pat->n_states; i++) {
++                      const struct imap_match_nfa_state *state;
+ 
+-      if (*pattern != '*') {
+-              /* handle the pattern up to the first '*' */
+-              ret = match_sub(ctx, &data, &pattern);
+-              if (ret != IMAP_MATCH_YES || *pattern == '\0')
+-                      return ret;
+-      }
++                      if (!cur[i])
++                              continue;
+ 
+-      match = IMAP_MATCH_CHILDREN;
+-      while (*pattern == '*') {
+-              pattern++;
++                      state = &pat->states[i];
++                      switch (state->type) {
++                      case IMAP_MATCH_NFA_LITERAL:
++                              if (literal_matches(state, ch, inboxcase_pos))
++                                      next[i + 1] = TRUE;
++                              break;
++                      case IMAP_MATCH_NFA_PERCENT:
++                              if (!ch_is_sep)
++                                      next[i] = TRUE;
++                              break;
++                      case IMAP_MATCH_NFA_STAR:
++                              next[i] = TRUE;
++                              break;
++                      }
++              }
+ 
+-              if (*pattern == '\0')
+-                      return IMAP_MATCH_YES;
++              {
++                      bool *tmp = cur;
++                      cur = next;
++                      next = tmp;
++              }
++              nfa_eps_close(pat, cur);
++              data_ends_with_sep = ch_is_sep;
++      }
+ 
+-              while (*data != '\0') {
+-                      if (CMP_CUR_CHR(ctx, data, pattern)) {
+-                              ret = match_sub(ctx, &data, &pattern);
+-                              if (ret == IMAP_MATCH_YES)
+-                                      break;
+-                              match |= ret;
++      if (cur[pat->n_states])
++              result = IMAP_MATCH_YES;
++      else {
++              unsigned int i;
++              bool has_nonaccept = FALSE;
++              bool has_sep_accept = FALSE;
++
++              for (i = 0; i < pat->n_states; i++) {
++                      if (!cur[i])
++                              continue;
++                      has_nonaccept = TRUE;
++                      if (pat->states[i].sep_accept) {
++                              has_sep_accept = TRUE;
++                              break;
+                       }
+-
+-                      data++;
+               }
++
++              if (has_nonaccept && (data_ends_with_sep || has_sep_accept))
++                      result |= IMAP_MATCH_CHILDREN;
++              if (parent_flag)
++                      result |= IMAP_MATCH_PARENT;
+       }
+ 
+-      return *data == '\0' && *pattern == '\0' ?
+-              IMAP_MATCH_YES : match;
++      return result;
+ }
+ 
+ enum imap_match_result
+ imap_match(struct imap_match_glob *glob, const char *data)
+ {
+-      struct imap_match_context ctx;
+       unsigned int i;
+       enum imap_match_result ret, match;
+ 
+       match = IMAP_MATCH_NO;
+-      ctx.sep = glob->sep;
+       for (i = 0; glob->patterns[i].pattern != NULL; i++) {
+-              ctx.inboxcase = glob->patterns[i].inboxcase;
+-
+-              ret = imap_match_pattern(&ctx, data, glob->patterns[i].pattern);
++              T_BEGIN {
++                      ret = imap_match_pattern_run(&glob->patterns[i], data,
++                                                   glob->sep,
++                                                   glob->patterns[i].inboxcase);
++              } T_END;
+               if (ret == IMAP_MATCH_YES)
+                       return IMAP_MATCH_YES;
+ 
+diff --git a/src/lib-imap/test-imap-match.c b/src/lib-imap/test-imap-match.c
+index df911dae50..88459ea9c3 100644
+--- a/src/lib-imap/test-imap-match.c
++++ b/src/lib-imap/test-imap-match.c
+@@ -2,6 +2,7 @@
+ 
+ #include "lib.h"
+ #include "imap-match.h"
++#include "str.h"
+ #include "test-common.h"
+ 
+ struct test_imap_match {
+@@ -88,6 +89,56 @@ static void test_imap_match(void)
+       test_end();
+ }
+ 
++static void test_imap_match_no_redos(void)
++{
++      struct imap_match_glob *glob;
++      pool_t pool;
++      const unsigned int wildcard_count = 1000;
++      string_t *mailbox_name, *pattern;
++      unsigned int i;
++
++      pool = pool_alloconly_create("imap match redos", 1024);
++      test_begin("imap match no redos");
++
++      mailbox_name = str_new(pool, 256);
++      for (i = 0; i < 255; i++)
++              str_append_c(mailbox_name, 'a');
++
++      pattern = str_new(pool, wildcard_count * 2 + 10);
++      for (i = 0; i < 255; i++)
++              str_append(pattern, "%a");
++      glob = imap_match_init(pool, str_c(pattern), FALSE, '/');
++      test_assert(imap_match(glob, str_c(mailbox_name)) == IMAP_MATCH_YES);
++
++      str_truncate(pattern, 0);
++      for (i = 0; i < wildcard_count; i++)
++              str_append(pattern, "%a");
++      glob = imap_match_init(pool, str_c(pattern), FALSE, '/');
++      test_assert(imap_match(glob, str_c(mailbox_name)) == IMAP_MATCH_NO);
++
++      str_append_c(pattern, 'b');
++      glob = imap_match_init(pool, str_c(pattern), FALSE, '/');
++      test_assert(imap_match(glob, str_c(mailbox_name)) == IMAP_MATCH_NO);
++
++      str_truncate(pattern, 0);
++      for (i = 0; i < wildcard_count; i++)
++              str_append(pattern, "*a");
++      str_append_c(pattern, 'b');
++      glob = imap_match_init(pool, str_c(pattern), FALSE, '/');
++      test_assert(imap_match(glob, str_c(mailbox_name)) == IMAP_MATCH_CHILDREN);
++      p_clear(pool);
++
++      glob = imap_match_init(pool, "%a%a%a%a%ab", FALSE, '/');
++      test_assert(imap_match(glob, "aaaaab") == IMAP_MATCH_YES);
++      p_clear(pool);
++
++      glob = imap_match_init(pool, "%a%a%a%a%ab", FALSE, '/');
++      test_assert(imap_match(glob, "aaaaa") == IMAP_MATCH_NO);
++
++      pool_unref(&pool);
++      test_end();
++}
++
+ static void test_imap_match_globs_equal(void)
+ {
+       struct imap_match_glob *glob;
+@@ -120,6 +171,7 @@ int main(void)
+ {
+       static void (*const test_functions[])(void) = {
+               test_imap_match,
++              test_imap_match_no_redos,
+               test_imap_match_globs_equal,
+               NULL
+       };
+-- 
+2.47.3
+
diff --git a/patches/0001-lib-imap-urlauth-Fix-leaking-uninitialized-memory-in.patch b/patches/0001-lib-imap-urlauth-Fix-leaking-uninitialized-memory-in.patch
new file mode 100644 (file)
index 0000000..3e5fbc6
--- /dev/null
@@ -0,0 +1,35 @@
+From 366ca92f916953c2e475ff20dd7d21fcca5a1987 Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Wed, 6 May 2026 14:53:41 +0000
+Subject: [PATCH] lib-imap-urlauth: Fix leaking uninitialized memory into
+ client error message
+
+imap_urlauth_fetch_parsed() called imap_msgpart_url_open_mailbox() with
+client_error_r and then, on the ret==0 (mailbox-not-found) branch,
+formatted a separate uninitialized local "error" pointer with
+t_strdup_printf("Invalid URLAUTH: %s", error). The %s read process
+stack memory until a NUL byte and sent it to the authenticated IMAP
+client inside the "* NO Failed to fetch URLAUTH ..." response.
+
+Broken by bb193c273e63ffa42c5c0b51ecd8860398e3beab
+---
+ src/lib-imap-urlauth/imap-urlauth.c | 3 ++-
+ 1 file changed, 2 insertions(+), 1 deletion(-)
+
+diff --git a/src/lib-imap-urlauth/imap-urlauth.c b/src/lib-imap-urlauth/imap-urlauth.c
+index 3cdcc21bb6..21f4520a93 100644
+--- a/src/lib-imap-urlauth/imap-urlauth.c
++++ b/src/lib-imap-urlauth/imap-urlauth.c
+@@ -513,7 +513,8 @@ int imap_urlauth_fetch_parsed(struct imap_urlauth_context *uctx,
+       }
+ 
+       if ((ret = imap_msgpart_url_open_mailbox(mpurl, &box, error_code_r,
+-                                               client_error_r)) < 0) {
++                                               &error)) < 0) {
++              *client_error_r = t_strdup_printf("Invalid URLAUTH: %s", error);
+               imap_msgpart_url_free(&mpurl);
+               return -1;
+       }
+-- 
+2.47.3
+
diff --git a/patches/0001-lib-lda-mail-deliver-Autocreate-delivery-mailbox-via.patch b/patches/0001-lib-lda-mail-deliver-Autocreate-delivery-mailbox-via.patch
new file mode 100644 (file)
index 0000000..b962485
--- /dev/null
@@ -0,0 +1,63 @@
+From 3a25b72230003ca7e133dc1117cc5e0a2f006ef3 Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Thu, 11 Jun 2026 17:20:32 +0000
+Subject: [PATCH 1/2] lib-lda: mail-deliver - Autocreate delivery mailbox via
+ explicit mailbox_create()
+
+Instead of relying on MAILBOX_FLAG_AUTO_CREATE to have mailbox_open()
+implicitly create the target, open it and, on MAIL_ERROR_NOTFOUND,
+create it explicitly when lda_mailbox_autocreate is enabled. This makes
+the creation go through the normal mailbox_create() path (including ACL
+checks) and removes one of the two users of the autocreate mailbox flags.
+---
+ src/lib-lda/mail-deliver.c | 29 ++++++++++++++++++++++++-----
+ 1 file changed, 24 insertions(+), 5 deletions(-)
+
+diff --git a/src/lib-lda/mail-deliver.c b/src/lib-lda/mail-deliver.c
+index 1001fed76b..605353bb8d 100644
+--- a/src/lib-lda/mail-deliver.c
++++ b/src/lib-lda/mail-deliver.c
+@@ -244,16 +244,35 @@ int mail_deliver_save_open(struct mail_deliver_save_open_context *ctx,
+               return -1;
+       }
+ 
+-      if (ctx->lda_mailbox_autocreate)
+-              flags |= MAILBOX_FLAG_AUTO_CREATE;
+-      if (ctx->lda_mailbox_autosubscribe)
+-              flags |= MAILBOX_FLAG_AUTO_SUBSCRIBE;
+       *box_r = box = mailbox_alloc_for_user(ctx->user, name, flags);
+ 
+       if (mailbox_open(box) == 0)
+               return 0;
+       *error_str_r = mailbox_get_last_internal_error(box, error_r);
+-      return -1;
++
++      /* Autocreate the mailbox if it does not exist yet and
++         lda_mailbox_autocreate is enabled. */
++      if (*error_r != MAIL_ERROR_NOTFOUND || !ctx->lda_mailbox_autocreate)
++              return -1;
++
++      if (mailbox_create(box, NULL, FALSE) < 0) {
++              enum mail_error error;
++
++              *error_str_r = mailbox_get_last_internal_error(box, &error);
++              if (error != MAIL_ERROR_EXISTS) {
++                      *error_r = error;
++                      return -1;
++              }
++              /* Someone else just created it; fall through and open it. */
++      } else if (ctx->lda_mailbox_autosubscribe) {
++              (void)mailbox_set_subscribed(box, TRUE);
++      }
++
++      if (mailbox_open(box) < 0) {
++              *error_str_r = mailbox_get_last_internal_error(box, error_r);
++              return -1;
++      }
++      return 0;
+ }
+ 
+ static bool mail_deliver_check_duplicate(struct mail_deliver_session *session,
+-- 
+2.39.5
+
diff --git a/patches/0001-lib-mail-Reset-charset-translation-buffer-between-MI.patch b/patches/0001-lib-mail-Reset-charset-translation-buffer-between-MI.patch
new file mode 100644 (file)
index 0000000..c65010f
--- /dev/null
@@ -0,0 +1,105 @@
+From 3d300ea1f26efd7f0207c5e3efd81f644ca03bf9 Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Sat, 28 Feb 2026 10:27:19 +0200
+Subject: [PATCH 01/14] lib-mail: Reset charset translation buffer between MIME
+ parts
+
+If MIME part ended with an incomplete charset translation, the buffer was
+kept for the next MIME part. This could have produced garbage in the next
+MIME part, or a crash.
+
+Fixes:
+Panic: file message-decoder.c: line 232 (translation_buf_decode): assertion failed: (orig_size < CHARSET_MAX_PENDING_BUF_SIZE)
+---
+ src/lib-mail/message-decoder.c      |  4 +++
+ src/lib-mail/test-message-decoder.c | 52 +++++++++++++++++++++++++++++
+ 2 files changed, 56 insertions(+)
+
+diff --git a/src/lib-mail/message-decoder.c b/src/lib-mail/message-decoder.c
+index 845b3d5e3a..4eea9994a2 100644
+--- a/src/lib-mail/message-decoder.c
++++ b/src/lib-mail/message-decoder.c
+@@ -257,6 +257,10 @@ message_decode_body_init_charset(struct message_decoder_context *ctx,
+       if (ctx->binary_input)
+               return;
+ 
++      /* If some input was left untranslated in the previous MIME part,
++         it needs to be discarded now so it won't affect the next part. */
++      ctx->translation_size = 0;
++
+       if (ctx->charset_trans != NULL && ctx->content_charset != NULL &&
+           strcasecmp(ctx->content_charset, ctx->charset_trans_charset) == 0) {
+               /* already have the correct translation selected */
+diff --git a/src/lib-mail/test-message-decoder.c b/src/lib-mail/test-message-decoder.c
+index edf9210cda..83c267187b 100644
+--- a/src/lib-mail/test-message-decoder.c
++++ b/src/lib-mail/test-message-decoder.c
+@@ -498,6 +498,57 @@ UNICODE_REPLACEMENT_CHAR_UTF8;
+       test_end();
+ }
+ 
++static void test_message_decoder_charset_mime_part_change(void)
++{
++      static const unsigned char test_message_input[] =
++"Content-Type: multipart/mixed; boundary=\"1\"\n"
++"MIME-Version: 1.0\n\n"
++"--1\n"
++"Content-Type: text/plain; charset=utf-8\n\n"
++"\xc3\n"
++"--1\n"
++"Content-Type: text/plain; charset=utf-8\n\n"
++"\xa4\n"
++"--1--\n";
++
++      static const char *test_message_output =
++              UNICODE_REPLACEMENT_CHAR_UTF8;
++
++      test_begin("message decoder charset - mime part change");
++
++      const struct message_parser_settings parser_set = { .flags = 0, };
++      struct message_parser_ctx *parser;
++      struct message_decoder_context *decoder;
++      struct message_part *parts;
++      struct message_block input, output;
++      struct istream *istream;
++      string_t *str_out = t_str_new(20);
++      int ret;
++
++      pool_t pool = pool_alloconly_create("message parser", 10240);
++      istream = test_istream_create_data(test_message_input,
++                                         sizeof(test_message_input)-1);
++      parser = message_parser_init(pool, istream, &parser_set);
++      decoder = message_decoder_init(NULL, 0);
++
++      while ((ret = message_parser_parse_next_block(parser, &input)) > 0) {
++              message_part_data_parse_from_header(pool, input.part, input.hdr);
++              if (message_decoder_decode_next_block(decoder, &input, &output) &&
++                  output.hdr == NULL && output.size > 0)
++                      str_append_data(str_out, output.data, output.size);
++      }
++
++      test_assert(ret == -1);
++      test_assert_strcmp(test_message_output, str_c(str_out));
++      message_decoder_deinit(&decoder);
++      message_parser_deinit(&parser, &parts);
++      test_assert(istream->stream_errno == 0);
++
++      i_stream_unref(&istream);
++      pool_unref(&pool);
++      test_end();
++}
++
+ int main(void)
+ {
+       static void (*const test_functions[])(void) = {
+@@ -507,6 +558,7 @@ int main(void)
+               test_message_decoder_content_transfer_encoding,
+               test_message_decoder_invalid_content_transfer_encoding,
+               test_message_decoder_charset,
++              test_message_decoder_charset_mime_part_change,
+               NULL
+       };
+       return test_run(test_functions);
+-- 
+2.47.3
+
diff --git a/patches/0001-lib-mail-o_stream_dot_sendv-Use-I_MIN.patch b/patches/0001-lib-mail-o_stream_dot_sendv-Use-I_MIN.patch
new file mode 100644 (file)
index 0000000..9292056
--- /dev/null
@@ -0,0 +1,23 @@
+From fc74e9b79107879c9f6f714c2c8becb533ba07d6 Mon Sep 17 00:00:00 2001
+From: Marco Bettini <marco.bettini@open-xchange.com>
+Date: Fri, 10 Apr 2026 08:53:36 +0000
+Subject: [PATCH 1/4] lib-mail: o_stream_dot_sendv() - Use I_MIN()
+
+---
+ src/lib-mail/ostream-dot.c | 3 +--
+ 1 file changed, 1 insertion(+), 2 deletions(-)
+
+Index: trixie/src/lib-mail/ostream-dot.c
+===================================================================
+--- trixie.orig/src/lib-mail/ostream-dot.c
++++ trixie/src/lib-mail/ostream-dot.c
+@@ -198,8 +198,7 @@ o_stream_dot_sendv(struct ostream_privat
+               if (max_bytes == 0)
+                       break;
+               i_assert(p <= pend);
+-              chunk = ((size_t)(p-data) >= max_bytes ?
+-                              max_bytes : (size_t)(p - data));
++              chunk = I_MIN((size_t)(p - data), max_bytes);
+               if (chunk > 0) {
+                       iovn.iov_base = data;
+                       iovn.iov_len = chunk;
diff --git a/patches/0001-lib-mail-ostream-dot-Optimize-stream-writing.patch b/patches/0001-lib-mail-ostream-dot-Optimize-stream-writing.patch
new file mode 100644 (file)
index 0000000..0f367ba
--- /dev/null
@@ -0,0 +1,60 @@
+From e94a6c3cb984f9d1e5fe51714ea5015205d79fd1 Mon Sep 17 00:00:00 2001
+From: Aki Tuomi <aki.tuomi@open-xchange.com>
+Date: Mon, 26 Jan 2026 14:55:08 +0200
+Subject: [PATCH] lib-mail: ostream-dot - Optimize stream writing
+
+Use i_memcspn() to figure out how much we can skip.
+---
+ src/lib-mail/ostream-dot.c | 14 +++++++++++++-
+ 1 file changed, 13 insertions(+), 1 deletion(-)
+
+Index: trixie/src/lib-mail/ostream-dot.c
+===================================================================
+--- trixie.orig/src/lib-mail/ostream-dot.c
++++ trixie/src/lib-mail/ostream-dot.c
+@@ -108,9 +108,17 @@ o_stream_dot_sendv(struct ostream_privat
+               for (; p < pend && (size_t)(p-data)+2 < max_bytes; p++) {
+                       char add = 0;
+ 
++                      size = pend - p;
+                       switch (dstream->state) {
+                       /* none */
+-                      case STREAM_STATE_NONE:
++                      case STREAM_STATE_NONE: {
++                              size_t maxlen = I_MIN(size, max_bytes - ((size_t)(p - data) + 2));
++                              p = CONST_PTR_OFFSET(p, i_memcspn(p, maxlen, "\r\n", 2));
++                              i_assert(p <= pend);
++                              if (p == pend) {
++                                      p--;
++                                      continue;
++                              }
+                               switch (*p) {
+                               case '\n':
+                                       dstream->state = STREAM_STATE_CRLF;
+@@ -122,8 +130,10 @@ o_stream_dot_sendv(struct ostream_privat
+                                       break;
+                               }
+                               break;
++                      }
+                       /* got CR */
+                       case STREAM_STATE_CR:
++                              i_assert(p < pend);
+                               switch (*p) {
+                               case '\r':
+                                       break;
+@@ -138,6 +148,7 @@ o_stream_dot_sendv(struct ostream_privat
+                       /* got CRLF, or the first line */
+                       case STREAM_STATE_INIT:
+                       case STREAM_STATE_CRLF:
++                              i_assert(p < pend);
+                               switch (*p) {
+                               case '\r':
+                                       dstream->state = STREAM_STATE_CR;
+@@ -186,6 +197,7 @@ o_stream_dot_sendv(struct ostream_privat
+ 
+               if (max_bytes == 0)
+                       break;
++              i_assert(p <= pend);
+               chunk = ((size_t)(p-data) >= max_bytes ?
+                               max_bytes : (size_t)(p - data));
+               if (chunk > 0) {
diff --git a/patches/0001-lib-managesieve-managesieve-parser-Fix-handling-of-l.patch b/patches/0001-lib-managesieve-managesieve-parser-Fix-handling-of-l.patch
new file mode 100644 (file)
index 0000000..4a6dc8e
--- /dev/null
@@ -0,0 +1,30 @@
+From b949a172750084895a9b6e8317787a2229f6a8f0 Mon Sep 17 00:00:00 2001
+From: Stephan Bosch <stephan.bosch@open-xchange.com>
+Date: Sat, 11 Apr 2026 14:12:08 +0200
+Subject: [PATCH 1/2] lib-managesieve: managesieve-parser - Fix handling of
+ lone CR character
+
+---
+ src/lib-managesieve/managesieve-parser.c | 9 +++++++++
+ 1 file changed, 9 insertions(+)
+
+Index: trixie/pigeonhole/src/lib-managesieve/managesieve-parser.c
+===================================================================
+--- trixie.orig/pigeonhole/src/lib-managesieve/managesieve-parser.c
++++ trixie/pigeonhole/src/lib-managesieve/managesieve-parser.c
+@@ -441,6 +441,15 @@ static bool managesieve_parser_read_arg(
+ 
+               switch (data[0]) {
+               case '\r':
++                      if (data_size == 1) {
++                              /* Wait for LF */
++                              return FALSE;
++                      }
++                      if (data[1] != '\n') {
++                              parser->error = "CR sent without LF";
++                              return FALSE;
++                      }
++                      /* Fall through */
+               case '\n':
+                       /* Unexpected end of line */
+                       parser->eol = TRUE;
diff --git a/patches/0001-lib-sieve-Add-per-user-sieve-rusage-file-for-cumulat.patch b/patches/0001-lib-sieve-Add-per-user-sieve-rusage-file-for-cumulat.patch
new file mode 100644 (file)
index 0000000..d0edbc6
--- /dev/null
@@ -0,0 +1,595 @@
+From da28fa5a900dc521a7f6cadf9320650dcad3cf18 Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Tue, 5 May 2026 16:18:34 +0000
+Subject: [PATCH 1/7] lib-sieve: Add per-user sieve-rusage file for cumulative
+ CPU tracking
+
+Cumulative resource usage was previously stored in each compiled Sieve
+binary's (.svbin) header. Because the binary file is keyed by script
+name, a user could reset their CPU budget by uploading the same script
+under a different name (PUTSCRIPT newname + SETACTIVE newname),
+RENAMESCRIPT, or DELETESCRIPT followed by PUTSCRIPT under another
+name. Each such operation produced a fresh .svbin with zero counter,
+allowing the sieve_max_cpu_time limit to be bypassed indefinitely.
+
+Track cumulative CPU per user instead, in <INBOX-namespace-root>/sieve-rusage.
+The file persists across all script renames, deletions, and re-uploads,
+since it is not tied to any particular script name. Tracking is enabled
+only for personal storages where the user's INBOX namespace has a
+filesystem path; admin/default storages are unaffected here.
+
+The on-disk format is a single ASCII line:
+
+    V1 <flags> <cpu-secs> <update-time>
+
+CPU time is rounded up to whole seconds when persisted; sub-second
+precision is preserved only for the in-memory accumulation within a
+single execution. The file is updated by writing a new copy via
+file_dotlock_open_mode() and replacing the original via
+file_dotlock_replace(). Concurrent writers are serialized through the
+dotlock; readers do not need a lock because the rename is atomic.
+
+The .svbin header still carries its original resource_usage struct as
+the active fallback when no per-user file is available; a follow-up
+commit drops that path.
+---
+ src/lib-sieve/Makefile.am             |   2 +
+ src/lib-sieve/sieve-binary-file.c     |  44 +++++
+ src/lib-sieve/sieve-binary.c          |  35 ++++
+ src/lib-sieve/sieve-binary.h          |   5 +
+ src/lib-sieve/sieve-rusage.c          | 266 ++++++++++++++++++++++++++
+ src/lib-sieve/sieve-rusage.h          |  32 ++++
+ src/lib-sieve/sieve-storage-private.h |  11 ++
+ src/lib-sieve/sieve-storage.c         |   2 +
+ src/lib-sieve/sieve.c                 |   5 +
+ 9 files changed, 402 insertions(+)
+ create mode 100644 src/lib-sieve/sieve-rusage.c
+ create mode 100644 src/lib-sieve/sieve-rusage.h
+
+Index: trixie/pigeonhole/src/lib-sieve/Makefile.am
+===================================================================
+--- trixie.orig/pigeonhole/src/lib-sieve/Makefile.am
++++ trixie/pigeonhole/src/lib-sieve/Makefile.am
+@@ -109,6 +109,7 @@ libdovecot_sieve_la_SOURCES = \
+       sieve-ast.c \
+       sieve-binary.c \
+       sieve-binary-file.c \
++      sieve-rusage.c \
+       sieve-binary-code.c \
+       sieve-binary-debug.c \
+       sieve-parser.c \
+@@ -158,6 +159,7 @@ headers = \
+       sieve-ast.h \
+       sieve-binary.h \
+       sieve-binary-private.h \
++      sieve-rusage.h \
+       sieve-parser.h \
+       sieve-address.h \
+       sieve-validator.h \
+Index: trixie/pigeonhole/src/lib-sieve/sieve-binary-file.c
+===================================================================
+--- trixie.orig/pigeonhole/src/lib-sieve/sieve-binary-file.c
++++ trixie/pigeonhole/src/lib-sieve/sieve-binary-file.c
+@@ -18,6 +18,9 @@
+ #include "sieve-extensions.h"
+ #include "sieve-code.h"
+ #include "sieve-script.h"
++#include "sieve-script-private.h"
++#include "sieve-storage-private.h"
++#include "sieve-rusage.h"
+ 
+ #include "sieve-binary-private.h"
+ 
+@@ -1008,15 +1011,56 @@ sieve_binary_file_do_update_resource_usa
+       return ret;
+ }
+ 
++static int
++sieve_binary_storage_update_resource_usage(struct sieve_binary *sbin,
++                                         struct sieve_storage *storage,
++                                         enum sieve_error *error_code_r)
++{
++      struct sieve_resource_usage delta;
++      struct sieve_resource_usage total;
++      uint32_t flags;
++
++      delta = sbin->rusage;
++      sieve_binary_get_resource_usage(sbin, &total);
++      flags = sbin->header.flags & SIEVE_BINARY_FLAG_RESOURCE_LIMIT;
++
++      if (!HAS_ALL_BITS(flags, SIEVE_BINARY_FLAG_RESOURCE_LIMIT) &&
++          !sieve_resource_usage_is_high(sbin->svinst, &total)) {
++              /* Nothing meaningful to persist */
++              sieve_resource_usage_init(&sbin->rusage);
++              sbin->rusage_updated = FALSE;
++              return 0;
++      }
++
++      if (sieve_rusage_storage_add(storage, &delta, flags) < 0) {
++              *error_code_r = SIEVE_ERROR_TEMP_FAILURE;
++              return -1;
++      }
++
++      sieve_resource_usage_init(&sbin->rusage);
++      sbin->rusage_updated = FALSE;
++      return 0;
++}
++
+ int sieve_binary_file_update_resource_usage(struct sieve_binary *sbin,
+                                           enum sieve_error *error_code_r)
+ {
++      struct sieve_storage *storage = NULL;
+       int fd, ret = 0;
+ 
+       sieve_error_args_init(&error_code_r, NULL);
+ 
+       sieve_binary_file_close(&sbin->file);
+ 
++      if (sbin->script != NULL && sbin->script->storage != NULL &&
++          sbin->script->storage->rusage_path != NULL)
++              storage = sbin->script->storage;
++
++      if (storage != NULL) {
++              return sieve_binary_storage_update_resource_usage(
++                      sbin, storage, error_code_r);
++      }
++
+       if (sbin->path == NULL)
+               return 0;
+       if (sbin->header.version_major != SIEVE_BINARY_VERSION_MAJOR) {
+Index: trixie/pigeonhole/src/lib-sieve/sieve-binary.c
+===================================================================
+--- trixie.orig/pigeonhole/src/lib-sieve/sieve-binary.c
++++ trixie/pigeonhole/src/lib-sieve/sieve-binary.c
+@@ -9,6 +9,7 @@
+ #include "hash.h"
+ #include "array.h"
+ #include "ostream.h"
++#include "ioloop.h"
+ #include "eacces-error.h"
+ #include "safe-mkstemp.h"
+ 
+@@ -16,6 +17,9 @@
+ #include "sieve-extensions.h"
+ #include "sieve-code.h"
+ #include "sieve-script.h"
++#include "sieve-script-private.h"
++#include "sieve-storage-private.h"
++#include "sieve-rusage.h"
+ 
+ #include "sieve-binary-private.h"
+ 
+@@ -190,6 +194,37 @@ void sieve_binary_close(struct sieve_bin
+  * Resource usage
+  */
+ 
++static struct sieve_storage *
++sieve_binary_get_user_storage(struct sieve_binary *sbin)
++{
++      if (sbin->script == NULL)
++              return NULL;
++      if (sbin->script->storage == NULL)
++              return NULL;
++      if (sbin->script->storage->rusage_path == NULL)
++              return NULL;
++      return sbin->script->storage;
++}
++
++void sieve_binary_apply_persisted_rusage(struct sieve_binary *sbin)
++{
++      struct sieve_storage *storage = sieve_binary_get_user_storage(sbin);
++      struct sieve_resource_usage rusage;
++      struct sieve_binary_header *header = &sbin->header;
++      uint32_t flags = 0;
++
++      if (storage == NULL)
++              return;
++      if (sieve_rusage_storage_load(storage, &rusage, &flags) <= 0)
++              return;
++
++      header->resource_usage.cpu_time_msecs = rusage.cpu_time_msecs;
++      header->resource_usage.update_time = ioloop_time;
++      header->flags |= (flags & SIEVE_BINARY_FLAG_RESOURCE_LIMIT);
++      sieve_resource_usage_init(&sbin->rusage);
++      sbin->rusage_updated = FALSE;
++}
++
+ void sieve_binary_get_resource_usage(struct sieve_binary *sbin,
+                                    struct sieve_resource_usage *rusage_r)
+ {
+Index: trixie/pigeonhole/src/lib-sieve/sieve-binary.h
+===================================================================
+--- trixie.orig/pigeonhole/src/lib-sieve/sieve-binary.h
++++ trixie/pigeonhole/src/lib-sieve/sieve-binary.h
+@@ -37,6 +37,11 @@ bool sieve_binary_record_resource_usage(
+       ATTR_NULL(1);
+ void sieve_binary_set_resource_usage(struct sieve_binary *sbin,
+                                    const struct sieve_resource_usage *rusage);
++
++/* Apply persisted per-user rusage and limit flag from sieve-rusage file
++   into the binary header. No-op when the storage has no per-user file. */
++void sieve_binary_apply_persisted_rusage(struct sieve_binary *sbin);
++
+ /*
+  * Accessors
+  */
+Index: trixie/pigeonhole/src/lib-sieve/sieve-rusage.c
+===================================================================
+--- /dev/null
++++ trixie/pigeonhole/src/lib-sieve/sieve-rusage.c
+@@ -0,0 +1,266 @@
++/* Copyright (c) 2026 Pigeonhole authors, see the included COPYING file
++ */
++
++#include "lib.h"
++#include "str.h"
++#include "strnum.h"
++#include "ioloop.h"
++#include "eacces-error.h"
++#include "file-dotlock.h"
++#include "mail-namespace.h"
++#include "mail-storage.h"
++#include "mailbox-list.h"
++
++#include "sieve-common.h"
++#include "sieve-settings.h"
++#include "sieve-storage-private.h"
++#include "sieve-rusage.h"
++
++#include <sys/types.h>
++#include <sys/stat.h>
++#include <unistd.h>
++#include <fcntl.h>
++
++#define SIEVE_RUSAGE_FILENAME "sieve-rusage"
++#define SIEVE_RUSAGE_VERSION_TAG "V1"
++#define SIEVE_RUSAGE_LOCK_TIMEOUT 10
++#define SIEVE_RUSAGE_LOCK_STALE_TIMEOUT 60
++
++/* Maximum size of the on-disk file. Single-line ASCII format with four
++   space-separated tokens; anything larger is treated as corrupt. */
++#define SIEVE_RUSAGE_MAX_FILE_SIZE 128
++
++struct sieve_rusage_record {
++      uint32_t flags;
++      uint32_t cpu_time_msecs;
++      time_t update_time;
++};
++
++void sieve_rusage_storage_init(struct sieve_storage *storage,
++                             struct mail_user *user)
++{
++      struct mail_namespace *ns;
++      const char *root;
++
++      if (!storage->is_personal || user == NULL)
++              return;
++
++      ns = mail_namespace_find_inbox(user->namespaces);
++      if (ns == NULL)
++              return;
++
++      if (!mailbox_list_get_root_path(ns->list, MAILBOX_LIST_PATH_TYPE_DIR,
++                                      &root) || root == NULL)
++              return;
++
++      storage->user = user;
++      storage->inbox_list = ns->list;
++      storage->rusage_path = p_strconcat(storage->pool, root, "/",
++                                         SIEVE_RUSAGE_FILENAME, NULL);
++}
++
++static bool
++sieve_rusage_parse(const char *line, struct sieve_rusage_record *rec)
++{
++      const char *const *tokens;
++      uint32_t cpu_secs;
++      bool ok = FALSE;
++
++      T_BEGIN {
++              tokens = t_strsplit_spaces(line, " \t");
++              if (tokens[0] == NULL ||
++                  strcmp(tokens[0], SIEVE_RUSAGE_VERSION_TAG) != 0)
++                      break;
++              if (tokens[1] == NULL || tokens[2] == NULL ||
++                  tokens[3] == NULL || tokens[4] != NULL)
++                      break;
++              if (str_to_uint32(tokens[1], &rec->flags) < 0)
++                      break;
++              if (str_to_uint32(tokens[2], &cpu_secs) < 0)
++                      break;
++              if (str_to_time(tokens[3], &rec->update_time) < 0)
++                      break;
++              if (cpu_secs > UINT32_MAX / 1000)
++                      rec->cpu_time_msecs = UINT32_MAX;
++              else
++                      rec->cpu_time_msecs = cpu_secs * 1000;
++              ok = TRUE;
++      } T_END;
++
++      return ok;
++}
++
++static int
++sieve_rusage_read(struct sieve_storage *storage,
++                struct sieve_rusage_record *rec_r)
++{
++      char buf[SIEVE_RUSAGE_MAX_FILE_SIZE + 1];
++      char *nl;
++      ssize_t ret;
++      int fd;
++
++      i_zero(rec_r);
++
++      fd = open(storage->rusage_path, O_RDONLY);
++      if (fd < 0) {
++              if (errno == ENOENT)
++                      return 0;
++              e_error(storage->event, "open(%s) failed: %m",
++                      storage->rusage_path);
++              return -1;
++      }
++
++      ret = read(fd, buf, sizeof(buf) - 1);
++      i_close_fd(&fd);
++      if (ret < 0) {
++              e_error(storage->event, "read(%s) failed: %m",
++                      storage->rusage_path);
++              return -1;
++      }
++      if (ret == 0)
++              return 0;
++      if (ret >= (ssize_t)(sizeof(buf) - 1)) {
++              e_warning(storage->event, "%s: file too large; resetting",
++                        storage->rusage_path);
++              return 0;
++      }
++      buf[ret] = '\0';
++
++      nl = strchr(buf, '\n');
++      if (nl != NULL)
++              *nl = '\0';
++
++      if (!sieve_rusage_parse(buf, rec_r)) {
++              e_warning(storage->event, "%s: malformed content; resetting",
++                        storage->rusage_path);
++              i_zero(rec_r);
++      }
++      return 0;
++}
++
++static void
++sieve_rusage_dotlock_settings(struct dotlock_settings *set_r)
++{
++      i_zero(set_r);
++      set_r->timeout = SIEVE_RUSAGE_LOCK_TIMEOUT;
++      set_r->stale_timeout = SIEVE_RUSAGE_LOCK_STALE_TIMEOUT;
++      set_r->use_excl_lock = TRUE;
++}
++
++int sieve_rusage_storage_load(struct sieve_storage *storage,
++                            struct sieve_resource_usage *rusage_r,
++                            uint32_t *flags_r)
++{
++      struct sieve_rusage_record rec;
++      unsigned int timeout;
++
++      sieve_resource_usage_init(rusage_r);
++      *flags_r = 0;
++
++      if (storage->rusage_path == NULL)
++              return 0;
++
++      if (sieve_rusage_read(storage, &rec) < 0)
++              return -1;
++
++      timeout = storage->svinst->set->resource_usage_timeout;
++      if (rec.update_time != 0 &&
++          (ioloop_time - rec.update_time) > (time_t)timeout) {
++              /* decayed */
++              return 1;
++      }
++
++      rusage_r->cpu_time_msecs = rec.cpu_time_msecs;
++      *flags_r = rec.flags;
++      return 1;
++}
++
++int sieve_rusage_storage_add(struct sieve_storage *storage,
++                           const struct sieve_resource_usage *delta_rusage,
++                           uint32_t flags_to_set)
++{
++      struct sieve_rusage_record rec;
++      struct dotlock_settings dlset;
++      struct dotlock *dotlock;
++      unsigned int timeout;
++      uint32_t old_cpu, cpu_secs;
++      string_t *out;
++      ssize_t wret;
++      int fd;
++
++      if (storage->rusage_path == NULL)
++              return 0;
++
++      sieve_rusage_dotlock_settings(&dlset);
++
++      /* The namespace root may not exist yet on the very first update for
++         a user (e.g. LDA refused execution before any mail was delivered).
++         Create it on demand so file_dotlock_open does not fail with ENOENT. */
++      if (storage->inbox_list != NULL &&
++          mailbox_list_mkdir_root(storage->inbox_list, NULL,
++                                  MAILBOX_LIST_PATH_TYPE_DIR) < 0)
++              return -1;
++
++      fd = file_dotlock_open_mode(&dlset, storage->rusage_path, 0,
++                                  0600, (uid_t)-1, (gid_t)-1, &dotlock);
++      if (fd == -1) {
++              if (errno == EACCES) {
++                      e_error(storage->event, "%s",
++                              eacces_error_get_creating("file_dotlock_open",
++                                                        storage->rusage_path));
++              } else {
++                      e_error(storage->event,
++                              "file_dotlock_open(%s) failed: %m",
++                              storage->rusage_path);
++              }
++              return -1;
++      }
++
++      if (sieve_rusage_read(storage, &rec) < 0) {
++              file_dotlock_delete(&dotlock);
++              return -1;
++      }
++
++      timeout = storage->svinst->set->resource_usage_timeout;
++      if (rec.update_time != 0 &&
++          (ioloop_time - rec.update_time) > (time_t)timeout) {
++              /* decayed: discard previous values */
++              rec.cpu_time_msecs = 0;
++              rec.flags = 0;
++      }
++
++      old_cpu = rec.cpu_time_msecs;
++      if ((UINT32_MAX - old_cpu) < delta_rusage->cpu_time_msecs)
++              rec.cpu_time_msecs = UINT32_MAX;
++      else
++              rec.cpu_time_msecs = old_cpu + delta_rusage->cpu_time_msecs;
++      rec.flags |= flags_to_set;
++      rec.update_time = ioloop_time;
++
++      /* Round CPU time up to whole seconds for persistence. */
++      if (rec.cpu_time_msecs >= UINT32_MAX - 999)
++              cpu_secs = UINT32_MAX / 1000;
++      else
++              cpu_secs = (rec.cpu_time_msecs + 999) / 1000;
++
++      out = t_str_new(64);
++      str_printfa(out, "%s %u %u %lld\n", SIEVE_RUSAGE_VERSION_TAG,
++                  rec.flags, cpu_secs, (long long)rec.update_time);
++
++      wret = write(fd, str_data(out), str_len(out));
++      if (wret != (ssize_t)str_len(out)) {
++              e_error(storage->event,
++                      "write(%s) failed: %m (wrote %zd/%zu)",
++                      storage->rusage_path, wret, str_len(out));
++              file_dotlock_delete(&dotlock);
++              return -1;
++      }
++
++      if (file_dotlock_replace(&dotlock, 0) < 0) {
++              e_error(storage->event,
++                      "file_dotlock_replace(%s) failed: %m",
++                      storage->rusage_path);
++              return -1;
++      }
++      return 1;
++}
+Index: trixie/pigeonhole/src/lib-sieve/sieve-rusage.h
+===================================================================
+--- /dev/null
++++ trixie/pigeonhole/src/lib-sieve/sieve-rusage.h
+@@ -0,0 +1,32 @@
++#ifndef SIEVE_RUSAGE_H
++#define SIEVE_RUSAGE_H
++
++#include "sieve-common.h"
++
++struct sieve_storage;
++struct mail_user;
++
++/* Per-user resource usage file lives at <INBOX-namespace-root>/sieve-rusage.
++   It survives script renames, deletions, and re-uploads under a different
++   name, preventing CPU-budget bypass. Tracking is enabled only for personal
++   user-controlled storage. */
++
++void sieve_rusage_storage_init(struct sieve_storage *storage,
++                             struct mail_user *user);
++
++/* Load persisted resource usage from the per-user file. Returns 1 on success,
++   0 if the storage has no rusage file (no INBOX namespace path), -1 on error.
++   Applies the resource_usage_timeout decay. flags_r receives sieve binary
++   header flags (RESOURCE_LIMIT) that were persisted. */
++int sieve_rusage_storage_load(struct sieve_storage *storage,
++                            struct sieve_resource_usage *rusage_r,
++                            uint32_t *flags_r);
++
++/* Atomically add delta_rusage to and OR flags_to_set into the persisted
++   per-user rusage file. Read-modify-write under fcntl WRLCK. Returns 1 on
++   success, 0 if no rusage file is configured, -1 on error. */
++int sieve_rusage_storage_add(struct sieve_storage *storage,
++                           const struct sieve_resource_usage *delta_rusage,
++                           uint32_t flags_to_set);
++
++#endif
+Index: trixie/pigeonhole/src/lib-sieve/sieve-storage-private.h
+===================================================================
+--- trixie.orig/pigeonhole/src/lib-sieve/sieve-storage-private.h
++++ trixie/pigeonhole/src/lib-sieve/sieve-storage-private.h
+@@ -118,6 +118,17 @@ struct sieve_storage {
+ 
+       struct mail_namespace *sync_inbox_ns;
+ 
++      /* Per-user resource usage tracking. Set when storage is personal and
++         the user's INBOX namespace has a filesystem path. NULL otherwise.
++         inbox_list is kept so the namespace root directory can be created
++         on demand before the rusage file is updated; LDA may close the
++         sieve binary (and therefore persist rusage) before any mail has
++         been delivered, in which case the namespace root may not exist
++         yet. */
++      struct mail_user *user;
++      struct mailbox_list *inbox_list;
++      const char *rusage_path;
++
+       enum sieve_storage_flags flags;
+ 
+       bool allows_synchronization:1;
+Index: trixie/pigeonhole/src/lib-sieve/sieve-storage.c
+===================================================================
+--- trixie.orig/pigeonhole/src/lib-sieve/sieve-storage.c
++++ trixie/pigeonhole/src/lib-sieve/sieve-storage.c
+@@ -17,6 +17,7 @@
+ 
+ #include "sieve-script-private.h"
+ #include "sieve-storage-private.h"
++#include "sieve-rusage.h"
+ 
+ #include <sys/types.h>
+ #include <sys/stat.h>
+@@ -777,6 +778,7 @@ int sieve_storage_create_personal(struct
+       if (ret == 0) {
+               i_assert(storage->is_personal);
+               (void)sieve_storage_sync_init(storage, user);
++              sieve_rusage_storage_init(storage, user);
+       } else if (*error_code_r != SIEVE_ERROR_TEMP_FAILURE &&
+                  (flags & SIEVE_STORAGE_FLAG_SYNCHRONIZING) == 0 &&
+                  (flags & SIEVE_STORAGE_FLAG_READWRITE) == 0) {
+Index: trixie/pigeonhole/src/lib-sieve/sieve.c
+===================================================================
+--- trixie.orig/pigeonhole/src/lib-sieve/sieve.c
++++ trixie/pigeonhole/src/lib-sieve/sieve.c
+@@ -30,6 +30,7 @@
+ #include "sieve-generator.h"
+ #include "sieve-interpreter.h"
+ #include "sieve-binary-dumper.h"
++#include "sieve-rusage.h"
+ 
+ #include "sieve.h"
+ #include "sieve-common.h"
+@@ -444,6 +445,8 @@ sieve_open_script_real(struct sieve_scri
+ 
+       /* Try to open the matching binary */
+       if (sieve_script_binary_load(script, &sbin, error_code_r) == 0) {
++              /* Per-user rusage file (if any) overrides binary header */
++              sieve_binary_apply_persisted_rusage(sbin);
+               sieve_binary_get_resource_usage(sbin, &rusage);
+ 
+               /* Ok, it exists; now let's see if it is up to date */
+@@ -474,6 +477,8 @@ sieve_open_script_real(struct sieve_scri
+                       sieve_script_label(script));
+ 
+               sieve_binary_set_resource_usage(sbin, &rusage);
++              /* Per-user rusage file (if any) overrides any carry-forward */
++              sieve_binary_apply_persisted_rusage(sbin);
+       }
+ 
+       /* Check whether binary can be executed. */
diff --git a/patches/0001-lib-sieve-actions-Split-mailbox-creation-into-sieve_.patch b/patches/0001-lib-sieve-actions-Split-mailbox-creation-into-sieve_.patch
new file mode 100644 (file)
index 0000000..184b384
--- /dev/null
@@ -0,0 +1,124 @@
+From 4010e318939b557b0cdf7da3ae0f320d408aea65 Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Thu, 11 Jun 2026 19:31:45 +0000
+Subject: [PATCH 1/2] lib-sieve: actions - Split mailbox creation into
+ sieve_act_store_create_mailbox()
+
+Pure refactoring with no functional change: move the mailbox_create() /
+subscribe / reopen sequence out of the "mailbox" extension's :create side
+effect (seff_mailbox_create_pre_execute) into a new reusable
+sieve_act_store_create_mailbox() helper.
+---
+ .../plugins/mailbox/tag-mailbox-create.c      | 32 +---------------
+ src/lib-sieve/sieve-actions.c                 | 38 +++++++++++++++++++
+ src/lib-sieve/sieve-actions.h                 |  5 +++
+ 3 files changed, 45 insertions(+), 30 deletions(-)
+
+Index: trixie/pigeonhole/src/lib-sieve/plugins/mailbox/tag-mailbox-create.c
+===================================================================
+--- trixie.orig/pigeonhole/src/lib-sieve/plugins/mailbox/tag-mailbox-create.c
++++ trixie/pigeonhole/src/lib-sieve/plugins/mailbox/tag-mailbox-create.c
+@@ -150,36 +150,8 @@ seff_mailbox_create_pre_execute(
+               return SIEVE_EXEC_FAILURE;
+       }
+ 
+-      trans->error = NULL;
+-      trans->error_code = MAIL_ERROR_NONE;
+-
+-      /* Create mailbox */
+-      if (mailbox_create(box, NULL, FALSE) < 0) {
+-              sieve_act_store_get_storage_error(aenv, trans);
+-              if (trans->error_code == MAIL_ERROR_EXISTS) {
+-                      trans->error = NULL;
+-                      trans->error_code = MAIL_ERROR_NONE;
+-              } else {
+-                      return (trans->error_code == MAIL_ERROR_TEMP ?
+-                              SIEVE_EXEC_TEMP_FAILURE : SIEVE_EXEC_FAILURE);
+-              }
+-      }
+-
+-      /* Subscribe to it if necessary */
+-      if (eenv->scriptenv->mailbox_autosubscribe) {
+-              (void)mailbox_list_set_subscribed(
+-                      mailbox_get_namespace(box)->list,
+-                      mailbox_get_name(box), TRUE);
+-      }
+-
+-      /* Try opening again */
+-      if (mailbox_open(box) < 0) {
+-              /* Failed definitively */
+-              sieve_act_store_get_storage_error(aenv, trans);
+-              return (trans->error_code == MAIL_ERROR_TEMP ?
+-                      SIEVE_EXEC_TEMP_FAILURE : SIEVE_EXEC_FAILURE);
+-      }
+-      return SIEVE_EXEC_OK;
++      /* Create the mailbox */
++      return sieve_act_store_create_mailbox(aenv, trans);
+ }
+ 
+ 
+Index: trixie/pigeonhole/src/lib-sieve/sieve-actions.c
+===================================================================
+--- trixie.orig/pigeonhole/src/lib-sieve/sieve-actions.c
++++ trixie/pigeonhole/src/lib-sieve/sieve-actions.c
+@@ -372,6 +372,44 @@ void sieve_act_store_get_storage_error(c
+                                               &trans->error_code));
+ }
+ 
++int sieve_act_store_create_mailbox(const struct sieve_action_exec_env *aenv,
++                                 struct act_store_transaction *trans)
++{
++      const struct sieve_execute_env *eenv = aenv->exec_env;
++      struct mailbox *box = trans->box;
++
++      trans->error = NULL;
++      trans->error_code = MAIL_ERROR_NONE;
++
++      /* Create mailbox */
++      if (mailbox_create(box, NULL, FALSE) < 0) {
++              sieve_act_store_get_storage_error(aenv, trans);
++              if (trans->error_code == MAIL_ERROR_EXISTS) {
++                      trans->error = NULL;
++                      trans->error_code = MAIL_ERROR_NONE;
++              } else {
++                      return (trans->error_code == MAIL_ERROR_TEMP ?
++                              SIEVE_EXEC_TEMP_FAILURE : SIEVE_EXEC_FAILURE);
++              }
++      }
++
++      /* Subscribe to it if necessary */
++      if (eenv->scriptenv->mailbox_autosubscribe) {
++              (void)mailbox_list_set_subscribed(
++                      mailbox_get_namespace(box)->list,
++                      mailbox_get_name(box), TRUE);
++      }
++
++      /* Try opening again */
++      if (mailbox_open(box) < 0) {
++              /* Failed definitively */
++              sieve_act_store_get_storage_error(aenv, trans);
++              return (trans->error_code == MAIL_ERROR_TEMP ?
++                      SIEVE_EXEC_TEMP_FAILURE : SIEVE_EXEC_FAILURE);
++      }
++      return SIEVE_EXEC_OK;
++}
++
+ static bool
+ act_store_mailbox_alloc(const struct sieve_action_exec_env *aenv,
+                       const char *mailbox, struct mailbox **box_r,
+Index: trixie/pigeonhole/src/lib-sieve/sieve-actions.h
+===================================================================
+--- trixie.orig/pigeonhole/src/lib-sieve/sieve-actions.h
++++ trixie/pigeonhole/src/lib-sieve/sieve-actions.h
+@@ -246,6 +246,11 @@ void sieve_act_store_add_flags(const str
+ void sieve_act_store_get_storage_error(const struct sieve_action_exec_env *aenv,
+                                      struct act_store_transaction *trans);
+ 
++/* Create the target mailbox of the store transaction (and subscribe to it
++   when configured). */
++int sieve_act_store_create_mailbox(const struct sieve_action_exec_env *aenv,
++                                 struct act_store_transaction *trans);
++
+ /*
+  * Redirect action
+  */
diff --git a/patches/0001-lib-sieve-sieve-binary-code-Fix-single-byte-oob-stac.patch b/patches/0001-lib-sieve-sieve-binary-code-Fix-single-byte-oob-stac.patch
new file mode 100644 (file)
index 0000000..b14ea91
--- /dev/null
@@ -0,0 +1,41 @@
+From 3f0af33abcc9911b586be9b5adc0afb3e9f18d25 Mon Sep 17 00:00:00 2001
+From: Stephan Bosch <stephan.bosch@open-xchange.com>
+Date: Fri, 10 Apr 2026 02:27:17 +0200
+Subject: [PATCH] lib-sieve: sieve-binary-code - Fix single byte oob stack
+ buffer write in sieve_binary_emit_integer()
+
+---
+ src/lib-sieve/sieve-binary-code.c | 2 +-
+ tests/test-size.svtest            | 9 +++++++++
+ 2 files changed, 10 insertions(+), 1 deletion(-)
+
+Index: trixie/pigeonhole/src/lib-sieve/sieve-binary-code.c
+===================================================================
+--- trixie.orig/pigeonhole/src/lib-sieve/sieve-binary-code.c
++++ trixie/pigeonhole/src/lib-sieve/sieve-binary-code.c
+@@ -124,7 +124,7 @@ sieve_size_t sieve_binary_emit_integer(s
+                                      sieve_number_t integer)
+ {
+       sieve_size_t address = _sieve_binary_block_get_size(sblock);
+-      uint8_t buffer[sizeof(sieve_number_t) + 1];
++      uint8_t buffer[sizeof(sieve_number_t) * 8 / 7 + 1];
+       int bufpos = sizeof(buffer) - 1;
+ 
+       /* Encode last byte [0xxxxxxx]; msb == 0 marks the last byte */
+Index: trixie/pigeonhole/tests/test-size.svtest
+===================================================================
+--- trixie.orig/pigeonhole/tests/test-size.svtest
++++ trixie/pigeonhole/tests/test-size.svtest
+@@ -72,3 +72,12 @@ test "Exact size" {
+       }
+ }
+ 
++/*
++ * TEST: Extreme size
++ */
++
++test "Extreme size" {
++      if size :over 18446744073709551615 {
++              test_fail "size :over matched extreme size";
++      }
++}
diff --git a/patches/0001-lib-sieve-sieve-storage-Add-is_personal-bit.patch b/patches/0001-lib-sieve-sieve-storage-Add-is_personal-bit.patch
new file mode 100644 (file)
index 0000000..24f1810
--- /dev/null
@@ -0,0 +1,72 @@
+From d82c2e624e4a9c364a6fa58fad0938ae5578660c Mon Sep 17 00:00:00 2001
+From: Stephan Bosch <stephan.bosch@open-xchange.com>
+Date: Tue, 25 Nov 2025 05:07:16 +0100
+Subject: [PATCH] lib-sieve: sieve-storage - Add is_personal bit
+
+Simpler than evaluating the type and personal storage is created only through
+sieve_storage_create_personal(). This mimics the bit for default storage.
+---
+ src/lib-sieve/sieve-storage-private.h           | 1 +
+ src/lib-sieve/sieve-storage.c                   | 3 ++-
+ src/lib-sieve/storage/file/sieve-file-storage.c | 6 ++----
+ 3 files changed, 5 insertions(+), 5 deletions(-)
+
+Index: trixie/pigeonhole/src/lib-sieve/sieve-storage-private.h
+===================================================================
+--- trixie.orig/pigeonhole/src/lib-sieve/sieve-storage-private.h
++++ trixie/pigeonhole/src/lib-sieve/sieve-storage-private.h
+@@ -121,6 +121,7 @@ struct sieve_storage {
+       enum sieve_storage_flags flags;
+ 
+       bool allows_synchronization:1;
++      bool is_personal:1;
+       bool is_default:1;
+ };
+ 
+Index: trixie/pigeonhole/src/lib-sieve/sieve-storage.c
+===================================================================
+--- trixie.orig/pigeonhole/src/lib-sieve/sieve-storage.c
++++ trixie/pigeonhole/src/lib-sieve/sieve-storage.c
+@@ -773,6 +773,7 @@ int sieve_storage_create_personal(struct
+                                       &storage, error_code_r, NULL);
+       if (ret == 0) {
+               (void)sieve_storage_sync_init(storage, user);
++              storage->is_personal = TRUE;
+       } else if (*error_code_r != SIEVE_ERROR_TEMP_FAILURE &&
+                  (flags & SIEVE_STORAGE_FLAG_SYNCHRONIZING) == 0 &&
+                  (flags & SIEVE_STORAGE_FLAG_READWRITE) == 0) {
+@@ -1017,7 +1018,7 @@ sieve_storage_get_default_script(struct
+ 
+       if (*error_code_r != SIEVE_ERROR_NOT_FOUND ||
+           (storage->flags & SIEVE_STORAGE_FLAG_SYNCHRONIZING) != 0 ||
+-          !sieve_storage_is_personal(storage))
++          !storage->is_personal)
+               return -1;
+ 
+       /* Not found; if this name maps to the default script,
+Index: trixie/pigeonhole/src/lib-sieve/storage/file/sieve-file-storage.c
+===================================================================
+--- trixie.orig/pigeonhole/src/lib-sieve/storage/file/sieve-file-storage.c
++++ trixie/pigeonhole/src/lib-sieve/storage/file/sieve-file-storage.c
+@@ -459,11 +459,9 @@ sieve_file_storage_init_from_settings(
+ 
+       /* Stat storage directory */
+ 
+-      bool is_personal = sieve_storage_is_personal(storage);
+-
+       if (storage_path != NULL && *storage_path != '\0') {
+               if (sieve_file_storage_stat(fstorage, storage_path) < 0) {
+-                      if (!is_personal ||
++                      if (!storage->is_personal ||
+                           storage->error_code != SIEVE_ERROR_NOT_FOUND)
+                               return -1;
+                       if ((storage->flags & SIEVE_STORAGE_FLAG_READWRITE) == 0) {
+@@ -511,7 +509,7 @@ sieve_file_storage_init_from_settings(
+       }
+ 
+       if ((active_path == NULL || *active_path == '\0') &&
+-          (is_personal ||
++          (storage->is_personal ||
+            (storage->flags & SIEVE_STORAGE_FLAG_READWRITE) != 0)) {
+               e_debug(storage->event,
+                       "Active script path is unconfigured; "
diff --git a/patches/0001-lib-sieve-sieve-storage-Fix-panic-caused-by-flawed-d.patch b/patches/0001-lib-sieve-sieve-storage-Fix-panic-caused-by-flawed-d.patch
new file mode 100644 (file)
index 0000000..7c3c042
--- /dev/null
@@ -0,0 +1,53 @@
+From 4d0cbfe6260d567ebc34a747c29bb4e140124d93 Mon Sep 17 00:00:00 2001
+From: Stephan Bosch <stephan.bosch@open-xchange.com>
+Date: Wed, 4 Mar 2026 21:53:36 +0100
+Subject: [PATCH] lib-sieve: sieve-storage - Fix panic caused by flawed
+ detection of personal storage type
+
+Recent commit d82c2e624e4a9c364a6fa58fad0938ae5578660c added the is_personal
+flag, but it is assigned too late. Fix this issue by assigning this flag in the
+earliest allocation function.
+
+This issue commonly surfaces when people forget to set an appropriate type for a
+script storage definition, in which case the "personal" type is the default.
+
+Panic was:
+Panic: file sieve-file-storage-active.c: line 33
+(sieve_file_storage_active_read_link): assertion failed: (fstorage->active_path != NULL)
+---
+ src/lib-sieve/sieve-storage.c | 7 +++++--
+ 1 file changed, 5 insertions(+), 2 deletions(-)
+
+Index: trixie/pigeonhole/src/lib-sieve/sieve-storage.c
+===================================================================
+--- trixie.orig/pigeonhole/src/lib-sieve/sieve-storage.c
++++ trixie/pigeonhole/src/lib-sieve/sieve-storage.c
+@@ -232,6 +232,9 @@ sieve_storage_alloc_from_class(struct si
+                       storage->pool, "auto:", storage->type, NULL);
+       }
+ 
++      if (strcasecmp(script_type, SIEVE_STORAGE_TYPE_PERSONAL) == 0)
++              storage->is_personal = TRUE;
++
+       storage->event = event;
+       event_ref(event);
+ 
+@@ -772,8 +775,8 @@ int sieve_storage_create_personal(struct
+                                       SIEVE_STORAGE_TYPE_PERSONAL, flags,
+                                       &storage, error_code_r, NULL);
+       if (ret == 0) {
++              i_assert(storage->is_personal);
+               (void)sieve_storage_sync_init(storage, user);
+-              storage->is_personal = TRUE;
+       } else if (*error_code_r != SIEVE_ERROR_TEMP_FAILURE &&
+                  (flags & SIEVE_STORAGE_FLAG_SYNCHRONIZING) == 0 &&
+                  (flags & SIEVE_STORAGE_FLAG_READWRITE) == 0) {
+@@ -1894,7 +1897,7 @@ bool sieve_storage_is_default(const stru
+ 
+ bool sieve_storage_is_personal(struct sieve_storage *storage)
+ {
+-      return (strcasecmp(storage->type, SIEVE_STORAGE_TYPE_PERSONAL) == 0);
++      return storage->is_personal;
+ }
+ 
+ /*
diff --git a/patches/0001-lib-sieve-storage-file-Refuse-symlinks-escaping-pers.patch b/patches/0001-lib-sieve-storage-file-Refuse-symlinks-escaping-pers.patch
new file mode 100644 (file)
index 0000000..5c4bf78
--- /dev/null
@@ -0,0 +1,267 @@
+From abfb0c1fe180f0edd5527c05cb7dd97840a31ea1 Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Mon, 4 May 2026 13:09:37 +0000
+Subject: [PATCH 1/2] lib-sieve: storage: file - Refuse symlinks escaping
+ personal storage directory
+
+Pigeonhole's file storage followed any symlink encountered while resolving a
+script path, including symlinks in personal (user-writable) storage whose
+target lay outside the storage directory. In some non-recommended
+configurations a user could exploit this through the include extension:
+an "include :personal name;" lookup of ~/sieve/name.sieve transparently
+followed a user-placed to e.g. another user's file readable by the mail
+process, leaking its contents (or causing it to be parsed as Sieve).
+Normally this shouldn't be possible, because sieve processes shouldn't
+have any more privileges to read files than the local system user creating
+the symlink.
+
+Open the canonical (realpath'd) personal storage directory at storage init
+time and keep an O_DIRECTORY|O_CLOEXEC fd to it. Resolve script content
+reads through this fd by routing sieve_file_script_get_stream() via a new
+sieve_file_storage_open_safe() wrapper around t_openat_safe(), which:
+
+ - opens each path component with O_NOFOLLOW so symlinks are detected
+   explicitly rather than transparently followed;
+ - follows symlinks only when their (recursively resolved) target stays
+   beneath dir_fd, refusing absolute targets and `..` past the storage
+   root with ELOOP;
+ - caps the symlink-hop count to bound resolution time.
+
+Anchoring at dir_fd makes the lookup TOCTOU-safe even when intermediate
+path components are mutated on disk concurrently: resolution stays
+relative to the original directory inode and the safe walker still rejects
+any target that leaves it.
+
+Apply the protection only when storage->is_personal is set; admin-managed
+global storage is trusted and may legitimately use cross-boundary symlinks.
+Single-file storages (is_file=TRUE) keep dir_fd at -1 and fall back to the
+existing open path. Also guard the dir_fd open with S_ISDIR() to handle the
+autodetect quirk where storage_path can refer to a regular file even when
+is_file is FALSE.
+---
+ .../storage/file/sieve-file-script.c          | 39 ++++++++--
+ .../storage/file/sieve-file-storage.c         | 74 +++++++++++++++++++
+ .../storage/file/sieve-file-storage.h         | 32 ++++++++
+ 3 files changed, 140 insertions(+), 5 deletions(-)
+
+Index: trixie/pigeonhole/src/lib-sieve/storage/file/sieve-file-script.c
+===================================================================
+--- trixie.orig/pigeonhole/src/lib-sieve/storage/file/sieve-file-script.c
++++ trixie/pigeonhole/src/lib-sieve/storage/file/sieve-file-script.c
+@@ -441,15 +441,44 @@ sieve_file_script_get_stream(struct siev
+ {
+       struct sieve_file_script *fscript =
+               container_of(script, struct sieve_file_script, script);
++      struct sieve_file_storage *fstorage =
++              container_of(script->storage, struct sieve_file_storage,
++                           storage);
+       struct stat st;
+       struct istream *result;
++      const char *error;
+       int fd;
+ 
+-      fd = open(fscript->path, O_RDONLY);
+-      if (fd < 0) {
+-              sieve_file_script_handle_error(fscript, "open", fscript->path,
+-                                             fscript->script.name);
+-              return -1;
++      /* For directory-based storage, open the script via the storage
++         directory fd so that path resolution refuses to follow symlinks
++         whose (recursive) target leaves the storage directory.
++         Single-file storages have no dir_fd, so fall back to plain open(). */
++      if (fstorage->dir_fd >= 0 && fscript->filename != NULL &&
++          *fscript->filename != '\0') {
++              if (sieve_file_storage_open_safe(fstorage, fscript->filename,
++                                               O_RDONLY, &fd, &error) < 0) {
++                      if (errno == ELOOP) {
++                              sieve_script_set_critical(
++                                      script,
++                                      "Failed to open sieve script: %s",
++                                      error);
++                              script->storage->error_code =
++                                      SIEVE_ERROR_NO_PERMISSION;
++                              return -1;
++                      }
++                      sieve_file_script_handle_error(fscript, "open",
++                                                     fscript->path,
++                                                     fscript->script.name);
++                      return -1;
++              }
++      } else {
++              fd = open(fscript->path, O_RDONLY);
++              if (fd < 0) {
++                      sieve_file_script_handle_error(fscript, "open",
++                                                     fscript->path,
++                                                     fscript->script.name);
++                      return -1;
++              }
+       }
+ 
+       if (fstat(fd, &st) != 0) {
+Index: trixie/pigeonhole/src/lib-sieve/storage/file/sieve-file-storage.c
+===================================================================
+--- trixie.orig/pigeonhole/src/lib-sieve/storage/file/sieve-file-storage.c
++++ trixie/pigeonhole/src/lib-sieve/storage/file/sieve-file-storage.c
+@@ -20,6 +20,7 @@
+ #include <stdio.h>
+ #include <unistd.h>
+ #include <ctype.h>
++#include <fcntl.h>
+ #include <utime.h>
+ #include <sys/time.h>
+ 
+@@ -42,6 +43,33 @@ sieve_file_storage_path_extend(struct si
+       return t_strconcat(path, "/", filename , NULL);
+ }
+ 
++int sieve_file_storage_open_safe(struct sieve_file_storage *fstorage,
++                               const char *path, int flags, int *fd_r,
++                               const char **error_r)
++{
++      const char *walk_error;
++      int fd;
++
++      *fd_r = -1;
++
++      if (fstorage->dir_fd < 0) {
++              *error_r = "storage directory fd not available";
++              errno = ENOTSUP;
++              return -1;
++      }
++
++      fd = t_openat_safe(fstorage->dir_fd, path, flags, &walk_error);
++      if (fd < 0) {
++              *error_r = t_strdup_printf(
++                      "Failed to open '%s/%s': %s",
++                      fstorage->path, path, walk_error);
++              return -1;
++      }
++
++      *fd_r = fd;
++      return 0;
++}
++
+ /*
+  *
+  */
+@@ -215,10 +243,19 @@ static struct sieve_storage *sieve_file_
+       fstorage = p_new(pool, struct sieve_file_storage, 1);
+       fstorage->storage = sieve_file_storage;
+       fstorage->storage.pool = pool;
++      fstorage->dir_fd = -1;
+ 
+       return &fstorage->storage;
+ }
+ 
++static void sieve_file_storage_destroy(struct sieve_storage *storage)
++{
++      struct sieve_file_storage *fstorage =
++              container_of(storage, struct sieve_file_storage, storage);
++
++      i_close_fd(&fstorage->dir_fd);
++}
++
+ static int
+ sieve_file_storage_get_full_path(struct sieve_file_storage *fstorage,
+                                const char **storage_path)
+@@ -436,6 +473,42 @@ sieve_file_storage_init_common(struct si
+                       fstorage->link_path =
+                               p_strdup(storage->pool, link_path);
+               }
++
++              /* For personal (user-writable) storage, open a fd to the
++                 canonical storage directory. This fd anchors TOCTOU-safe
++                 path resolution for script lookups: the inode it refers to
++                 cannot change underneath us, so even if the user mutates
++                 intermediate path components on disk afterwards, we still
++                 resolve relative to the original directory. The walker that
++                 uses this fd refuses script paths that escape the storage
++                 directory through symlinks or `..`, which would otherwise
++                 let an unprivileged user redirect e.g. include "name" to a
++                 file outside their own sieve storage.
++
++                 Non-personal (e.g. admin-configured global) storage is
++                 trusted; dir_fd stays unset there so legitimate
++                 admin-managed symlinks crossing the storage boundary keep
++                 working.
++
++                 Only open the fd when storage_path actually resolves to a
++                 directory: autodetect can leave is_file=FALSE while
++                 storage_path still points at a regular file (see the
++                 storage_path == active_path fallback in
++                 sieve_file_storage_do_autodetect()). In that case there is
++                 no directory to anchor to and lookups by name will fail
++                 with ENOENT later anyway. */
++              if (storage->is_personal &&
++                  S_ISDIR(fstorage->st.st_mode)) {
++                      fstorage->dir_fd = open(storage_path,
++                                              O_RDONLY | O_DIRECTORY |
++                                              O_CLOEXEC);
++                      if (fstorage->dir_fd < 0) {
++                              sieve_storage_set_critical(storage,
++                                      "Failed to open storage directory: "
++                                      "open(%s) failed: %m", storage_path);
++                              return -1;
++                      }
++              }
+       }
+ 
+       fstorage->path = p_strdup(storage->pool, storage_path);
+@@ -904,6 +977,7 @@ const struct sieve_storage sieve_file_st
+       .v = {
+               .alloc = sieve_file_storage_alloc,
+               .init = sieve_file_storage_init,
++              .destroy = sieve_file_storage_destroy,
+ 
+               .autodetect = sieve_file_storage_autodetect,
+ 
+Index: trixie/pigeonhole/src/lib-sieve/storage/file/sieve-file-storage.h
+===================================================================
+--- trixie.orig/pigeonhole/src/lib-sieve/storage/file/sieve-file-storage.h
++++ trixie/pigeonhole/src/lib-sieve/storage/file/sieve-file-storage.h
+@@ -41,6 +41,13 @@ struct sieve_file_storage {
+ 
+       time_t prev_mtime;
+ 
++      /* fd referencing the canonical (realpath'd) storage directory. Used to
++         anchor TOCTOU-safe path resolution for script files: lookups that
++         escape this directory via symlinks (or `..`) are refused. -1 if the
++         storage is a single-file storage or this protection is unavailable.
++       */
++      int dir_fd;
++
+       bool is_file:1;
+ };
+ 
+@@ -60,6 +67,31 @@ int sieve_file_storage_init_from_path(st
+ 
+ int sieve_file_storage_pre_modify(struct sieve_storage *storage);
+ 
++/* Open a script file that lives under the storage directory, refusing any
++   path resolution that escapes that directory through symlinks or `..`.
++
++   The path is resolved component-by-component using openat() relative to
++   fstorage->dir_fd, with O_NOFOLLOW per component. Symlinks are followed
++   only if their (recursively resolved) target also stays beneath dir_fd;
++   absolute symlink targets and `..` past the storage root are refused.
++
++   Anchoring the resolution at dir_fd makes the check TOCTOU-safe: even if
++   the user mutates path components on disk between calls, dir_fd still
++   refers to the original directory inode.
++
++   `flags` is OR-ed into the openat() call for the final component (e.g.
++   O_RDONLY). O_NOFOLLOW and O_CLOEXEC are added automatically.
++
++   Returns 0 on success and stores the new fd in *fd_r. Returns -1 on
++   failure with errno set; *error_r is set to a descriptive message.
++   errno=ELOOP indicates either too many symlinks or an attempted escape.
++   errno=ENOTSUP indicates fstorage->dir_fd is not available; the caller
++   may fall back to opening fstorage->path directly.
++ */
++int sieve_file_storage_open_safe(struct sieve_file_storage *fstorage,
++                               const char *path, int flags, int *fd_r,
++                               const char **error_r);
++
+ /* Active script */
+ 
+ int sieve_file_storage_active_replace_link(struct sieve_file_storage *fstorage,
diff --git a/patches/0001-lib-sieve-util-edit-mail-Fix-writing-to-freed-memory.patch b/patches/0001-lib-sieve-util-edit-mail-Fix-writing-to-freed-memory.patch
new file mode 100644 (file)
index 0000000..3eeaace
--- /dev/null
@@ -0,0 +1,128 @@
+From adcd0f9553c6d07142b97766fa0b53d56112c15c Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Tue, 14 Apr 2026 12:58:08 +0200
+Subject: [PATCH 1/3] lib-sieve: util: edit-mail - Fix writing to freed memory
+
+---
+ Makefile.am                                   |  1 +
+ src/lib-sieve/util/edit-mail.c                | 12 ++-
+ .../editheader/deleteheader-snapshot.svtest   | 76 +++++++++++++++++++
+ 3 files changed, 88 insertions(+), 1 deletion(-)
+ create mode 100644 tests/extensions/editheader/deleteheader-snapshot.svtest
+
+Index: trixie/pigeonhole/Makefile.am
+===================================================================
+--- trixie.orig/pigeonhole/Makefile.am
++++ trixie/pigeonhole/Makefile.am
+@@ -164,6 +164,7 @@ test_cases = \
+       tests/extensions/ihave/restrictions.svtest \
+       tests/extensions/editheader/addheader.svtest \
+       tests/extensions/editheader/deleteheader.svtest \
++      tests/extensions/editheader/deleteheader-snapshot.svtest \
+       tests/extensions/editheader/alternating.svtest \
+       tests/extensions/editheader/utf8.svtest \
+       tests/extensions/editheader/protected.svtest \
+Index: trixie/pigeonhole/src/lib-sieve/util/edit-mail.c
+===================================================================
+--- trixie.orig/pigeonhole/src/lib-sieve/util/edit-mail.c
++++ trixie/pigeonhole/src/lib-sieve/util/edit-mail.c
+@@ -890,7 +890,17 @@ static int edit_mail_headers_parse(struc
+               }
+       }
+ 
+-      /* Rebuild header index */
++      /* Rebuild header index. Reset first pointers before rebuilding so that
++         the actual first occurrence in the final list is used. Without this,
++         a snapshot's pre-set first pointer may refer to an appended field
++         that sits later in the list than a parsed field with the same header,
++         causing deleteheader to miss the parsed fields while still freeing
++         the header_index â€” leaving stale field_idx->header pointers. */
++      struct _header_index *hidx = edmail->headers_head;
++      while (hidx != NULL) {
++              hidx->first = NULL;
++              hidx = hidx->next;
++      }
+       current = edmail->header_fields_head;
+       while (current != NULL) {
+               if (current->header->first == NULL)
+Index: trixie/pigeonhole/tests/extensions/editheader/deleteheader-snapshot.svtest
+===================================================================
+--- /dev/null
++++ trixie/pigeonhole/tests/extensions/editheader/deleteheader-snapshot.svtest
+@@ -0,0 +1,76 @@
++require "vnd.dovecot.testsuite";
++require "editheader";
++require "fileinto";
++require "mailbox";
++
++/*
++ * Regression test: heap use-after-free in edit_mail_snapshot when
++ * deleteheader is called after a fileinto snapshot on an edit_mail
++ * whose headers have not yet been parsed.
++ */
++
++test_set "message" text:
++From: sender@example.com
++To: recipient@example.com
++Cc: original@example.com
++Subject: Test
++
++Body.
++.
++;
++
++test "deleteheader all: snapshot stale first pointer after parse" {
++      /* Add a Cc header while headers are not yet parsed.  The snapshot
++       * created by the next fileinto has H_clone_cc->first pointing at
++       * this appended field. */
++      addheader :last "Cc" "appended@example.com";
++
++      /* fileinto sets edit_snapshot=TRUE.  The next call to
++       * sieve_message_edit creates a snapshot (edmail_snap) and the
++       * fileinto action retains the pre-snapshot version for delivery. */
++      fileinto :create "folder1";
++
++      /* sieve_message_edit creates edmail_snap here.  edit_mail_headers_parse
++       * inserts the parsed "Cc: original@example.com" before the appended
++       * field in the list.  Without the fix the rebuild skips resetting
++       * ->first; the delete loop then only removes the appended field,
++       * frees H_clone_cc (index==0), and leaves the parsed field_idx with
++       * a dangling ->header pointer. */
++      deleteheader "Cc";
++
++      /* A second fileinto + edit operation takes another snapshot of the
++       * same edmail_snap.  edit_mail_snapshot walks header_fields_head and
++       * reads field_idx->header->header on the orphaned entry.
++       * Without the fix: use-after-free crash.
++       * With the fix: clean run, both Cc occurrences are correctly gone. */
++      fileinto :create "folder2";
++      deleteheader "Subject";
++
++      if not test_result_execute {
++              test_fail "failed to execute result";
++      }
++
++      /* folder1 was snapshotted before deleteheader "Cc": both Cc headers
++       * must be present. */
++      if not test_message :folder "folder1" 0 {
++              test_fail "message not stored in folder1";
++      }
++
++      if not header :is "Cc" "original@example.com" {
++              test_fail "original Cc missing in folder1 snapshot";
++      }
++
++      if not header :is "Cc" "appended@example.com" {
++              test_fail "appended Cc missing in folder1 snapshot";
++      }
++
++      /* folder2 was snapshotted after deleteheader "Cc": both Cc headers
++       * must be absent. */
++      if not test_message :folder "folder2" 0 {
++              test_fail "message not stored in folder2";
++      }
++
++      if exists "Cc" {
++              test_fail "Cc header not fully deleted in folder2 snapshot";
++      }
++}
diff --git a/patches/0001-lib-sql-Add-sql_result_new_error-helper.patch b/patches/0001-lib-sql-Add-sql_result_new_error-helper.patch
new file mode 100644 (file)
index 0000000..eeead61
--- /dev/null
@@ -0,0 +1,66 @@
+From 7aa8c147eb15009b56c85a00c33c2c0b972c1398 Mon Sep 17 00:00:00 2001
+From: Aki Tuomi <aki.tuomi@open-xchange.com>
+Date: Fri, 29 May 2026 11:21:00 +0000
+Subject: [PATCH] lib-sql: Add sql_result_new_error() helper
+
+---
+ src/lib-sql/sql-api.c | 43 +++++++++++++++++++++++++++++++++++++++++++
+ 1 file changed, 43 insertions(+)
+
+diff --git a/src/lib-sql/sql-api.c b/src/lib-sql/sql-api.c
+index 3d4518c3cc..4db6913229 100644
+--- a/src/lib-sql/sql-api.c
++++ b/src/lib-sql/sql-api.c
+@@ -66,6 +66,49 @@ ARRAY_TYPE(sql_drivers) sql_drivers;
+ static void sql_query_delayed_callback(struct sql_query_result_delayed *cb);
+ static void sql_commit_delayed_callback(struct sql_commit_result_delayed *cb);
+ 
++struct sql_result_error {
++      struct sql_result result;
++      char *error;
++};
++
++static void sql_result_error_free(struct sql_result *_result)
++{
++      struct sql_result_error *result =
++              container_of(_result, struct sql_result_error, result);
++      i_free(result->error);
++      i_free(result);
++}
++
++static int sql_result_error_next_row(struct sql_result *result ATTR_UNUSED)
++{
++      return -1;
++}
++
++static const char *
++sql_result_error_get_error(struct sql_result *_result)
++{
++      struct sql_result_error *result =
++              container_of(_result, struct sql_result_error, result);
++      return result->error;
++}
++
++static const struct sql_result_vfuncs sql_result_error_vfuncs = {
++      .free = sql_result_error_free,
++      .next_row = sql_result_error_next_row,
++      .get_error = sql_result_error_get_error,
++};
++
++static struct sql_result *sql_result_new_error(const char *error) ATTR_UNUSED;
++static struct sql_result *sql_result_new_error(const char *error)
++{
++      struct sql_result_error *result = i_new(struct sql_result_error, 1);
++      result->result.v = sql_result_error_vfuncs;
++      result->result.failed = TRUE;
++      result->result.refcount = 1;
++      result->error = i_strdup(error);
++      return &result->result;
++}
++
+ void sql_drivers_init_without_drivers(void)
+ {
+       i_array_init(&sql_drivers, 8);
+-- 
+2.47.3
+
diff --git a/patches/0001-lib-sql-Use-strchr-based-while-loop-to-fill-template.patch b/patches/0001-lib-sql-Use-strchr-based-while-loop-to-fill-template.patch
new file mode 100644 (file)
index 0000000..a953082
--- /dev/null
@@ -0,0 +1,100 @@
+From 98282f1a02d29d6fdd0b3db63ae398d0162fe7bd Mon Sep 17 00:00:00 2001
+From: Aki Tuomi <aki.tuomi@open-xchange.com>
+Date: Mon, 18 Aug 2025 08:30:42 +0300
+Subject: [PATCH] lib-sql: Use strchr() based while loop to fill template
+
+This is much faster than going one byte at a time.
+---
+ src/lib-sql/sql-api.c  | 26 ++++++++++++++------------
+ src/lib-sql/test-sql.c | 30 ++++++++++++++++++++++++++++++
+ 2 files changed, 44 insertions(+), 12 deletions(-)
+
+Index: trixie/src/lib-sql/sql-api.c
+===================================================================
+--- trixie.orig/src/lib-sql/sql-api.c
++++ trixie/src/lib-sql/sql-api.c
+@@ -400,22 +400,24 @@ const char *sql_statement_get_query(stru
+ {
+       string_t *query = t_str_new(128);
+       const char *const *args;
+-      unsigned int i, args_count, arg_pos = 0;
++      unsigned int args_count, arg_pos = 0;
++      const char *p0, *p1;
+ 
+       args = array_get(&stmt->args, &args_count);
+-
+-      for (i = 0; stmt->query_template[i] != '\0'; i++) {
+-              if (stmt->query_template[i] == '?') {
+-                      if (arg_pos >= args_count ||
+-                          args[arg_pos] == NULL) {
+-                              i_panic("lib-sql: Missing bind for arg #%u in statement: %s",
+-                                      arg_pos, stmt->query_template);
+-                      }
+-                      str_append(query, args[arg_pos++]);
+-              } else {
+-                      str_append_c(query, stmt->query_template[i]);
++      p0 = stmt->query_template;
++      while ((p1 = strchr(p0, '?')) != NULL) {
++              /* append until ? */
++              str_append_max(query, p0, (p1 - p0));
++              if (arg_pos >= args_count ||
++                  args[arg_pos] == NULL) {
++                      i_panic("lib-sql: Missing bind for arg #%u in statement: %s",
++                              arg_pos, stmt->query_template);
+               }
++              str_append(query, args[arg_pos++]);
++              p0 = p1 + 1;
+       }
++      str_append(query, p0);
++
+       if (arg_pos != args_count) {
+               i_panic("lib-sql: Too many bind args (%u) for statement: %s",
+                       args_count, stmt->query_template);
+Index: trixie/src/lib-sql/test-sql.c
+===================================================================
+--- trixie.orig/src/lib-sql/test-sql.c
++++ trixie/src/lib-sql/test-sql.c
+@@ -60,6 +60,24 @@ static void deinit_sql(struct sql_db **_
+       }; \
+       sql_driver_test_add_expected_result(sql, &result_1);
+ 
++#define setup_result_2(sql) \
++      struct test_driver_result_set rset_2 = { \
++              .rows = 2, \
++              .cols = 1, \
++              .col_names = (const char *[]){"foo", NULL}, \
++              .row_data = (const char **[]){ \
++                      (const char*[]){"value1", NULL}, \
++                      (const char*[]){"value2", NULL}, \
++              }, \
++      }; \
++      struct test_driver_result result_2 = { \
++              .nqueries = 1, \
++              .queries = (const char *[]){"SELECT foo FROM bar WHERE baz = 'foz'"}, \
++              .result = &rset_2 \
++      }; \
++      sql_driver_test_add_expected_result(sql, &result_2);
++
++
+ static void test_result_1(struct sql_result *cursor)
+ {
+       test_assert(sql_result_next_row(cursor) == SQL_RESULT_NEXT_OK);
+@@ -125,6 +143,18 @@ static void test_sql_stmt_prepared_api(v
+ 
+       test_result_1(cursor);
+ 
++      sql_result_unref(cursor);
++
++      setup_result_2(sql);
++
++      prep_stmt = sql_prepared_statement_init(sql, "SELECT foo FROM bar WHERE baz = ?");
++      stmt = sql_statement_init_prepared(prep_stmt);
++      sql_statement_bind_str(stmt, 0, "foz");
++      sql_prepared_statement_unref(&prep_stmt);
++      cursor = sql_statement_query_s(&stmt);
++
++      test_result_1(cursor);
++
+       sql_result_unref(cursor);
+ 
+       deinit_sql(&sql);
diff --git a/patches/0001-lib-sql-driver-sqlite-Use-sqlite3_snprintf-to-quote-.patch b/patches/0001-lib-sql-driver-sqlite-Use-sqlite3_snprintf-to-quote-.patch
new file mode 100644 (file)
index 0000000..68c0782
--- /dev/null
@@ -0,0 +1,51 @@
+From ff926d0c9288f343d18a3c9ec20cf1e252d52286 Mon Sep 17 00:00:00 2001
+From: Aki Tuomi <aki.tuomi@open-xchange.com>
+Date: Fri, 7 Nov 2025 09:21:01 +0200
+Subject: [PATCH] lib-sql: driver-sqlite - Use sqlite3_snprintf() to quote
+ values
+
+This does it the sqlite3 way.
+---
+ src/lib-sql/driver-sqlite.c | 29 +++++------------------------
+ 1 file changed, 5 insertions(+), 24 deletions(-)
+
+Index: trixie/src/lib-sql/driver-sqlite.c
+===================================================================
+--- trixie.orig/src/lib-sql/driver-sqlite.c
++++ trixie/src/lib-sql/driver-sqlite.c
+@@ -225,30 +225,11 @@ static const char *
+ driver_sqlite_escape_string(struct sql_db *_db ATTR_UNUSED,
+                           const char *string)
+ {
+-      const char *p;
+-      char *dest, *destbegin;
+-
+-      /* find the first ' */
+-      for (p = string; *p != '\''; p++) {
+-              if (*p == '\0')
+-                      return t_strdup_noconst(string);
+-      }
+-
+-      /* @UNSAFE: escape ' with '' */
+-      dest = destbegin = t_buffer_get((p - string) + strlen(string) * 2 + 1);
+-
+-      memcpy(dest, string, p - string);
+-      dest += p - string;
+-
+-      for (; *p != '\0'; p++) {
+-              *dest++ = *p;
+-              if (*p == '\'')
+-                      *dest++ = *p;
+-      }
+-      *dest++ = '\0';
+-      t_buffer_alloc(dest - destbegin);
+-
+-      return destbegin;
++      const size_t len = strlen(string) * 2 + 1;
++      char *escaped = t_malloc_no0(len);
++      if (sqlite3_snprintf(len, escaped, "%q", string) == NULL)
++              i_unreached();
++      return escaped;
+ }
+ 
+ static const char *
diff --git a/patches/0001-lib-sql-sql-api-Implement-async-calls-for-drivers-th.patch b/patches/0001-lib-sql-sql-api-Implement-async-calls-for-drivers-th.patch
new file mode 100644 (file)
index 0000000..70a2714
--- /dev/null
@@ -0,0 +1,247 @@
+From a78c015eae67a891a0c14b5434dd1f99838ae4cc Mon Sep 17 00:00:00 2001
+From: Aki Tuomi <aki.tuomi@open-xchange.com>
+Date: Mon, 6 Oct 2025 12:39:50 +0300
+Subject: [PATCH] lib-sql: sql-api - Implement async calls for drivers that
+ can't
+
+Changes the behaviour of asynchronous functions to be truly asynchronous
+even if the underlying driver isn't capable of doing this. Implemented
+by adding immediate timeouts to call the callbacks after returning from the
+synchronous function and ending up back to ioloop.
+---
+ src/lib-sql/sql-api-private.h |   7 +++
+ src/lib-sql/sql-api.c         | 103 ++++++++++++++++++++++++++++++++--
+ src/lib-sql/sql-api.h         |   4 +-
+ 3 files changed, 107 insertions(+), 7 deletions(-)
+
+Index: trixie/src/lib-sql/sql-api-private.h
+===================================================================
+--- trixie.orig/src/lib-sql/sql-api-private.h
++++ trixie/src/lib-sql/sql-api-private.h
+@@ -84,11 +84,15 @@ struct sql_db_vfuncs {
+       const char *(*escape_string)(struct sql_db *db, const char *string);
+ 
+       void (*exec)(struct sql_db *db, const char *query);
++      /* Only implement this if the driver can really do asynchronous callbacks,
++         otherwise let sql API handle it. */
+       void (*query)(struct sql_db *db, const char *query,
+                     sql_query_callback_t *callback, void *context);
+       struct sql_result *(*query_s)(struct sql_db *db, const char *query);
+ 
+       struct sql_transaction_context *(*transaction_begin)(struct sql_db *db);
++      /* Only implement this if the driver can really do asynchronous callbacks,
++         otherwise let sql API handle it. */
+       void (*transaction_commit)(struct sql_transaction_context *ctx,
+                                  sql_commit_callback_t *callback,
+                                  void *context);
+@@ -154,6 +158,9 @@ struct sql_db {
+       struct event *event;
+       HASH_TABLE(char *, struct sql_prepared_statement *) prepared_stmt_hash;
+ 
++      struct sql_query_result_delayed *query_delayed_list;
++      struct sql_commit_result_delayed *commit_delayed_list;
++
+       enum sql_db_state state;
+       /* last time we started connecting to this server
+          (which may or may not have succeeded) */
+Index: trixie/src/lib-sql/sql-api.c
+===================================================================
+--- trixie.orig/src/lib-sql/sql-api.c
++++ trixie/src/lib-sql/sql-api.c
+@@ -4,6 +4,7 @@
+ #include "array.h"
+ #include "ioloop.h"
+ #include "hash.h"
++#include "llist.h"
+ #include "str.h"
+ #include "time-util.h"
+ #include "settings.h"
+@@ -12,6 +13,28 @@
+ 
+ #include <time.h>
+ 
++struct sql_query_result_delayed {
++      struct sql_query_result_delayed *prev, *next;
++
++      struct sql_db *db;
++      struct timeout *to;
++
++      struct sql_result *result;
++      sql_query_callback_t *callback;
++      void *context;
++};
++
++struct sql_commit_result_delayed {
++      struct sql_commit_result_delayed *prev, *next;
++
++      struct sql_db *db;
++      struct timeout *to;
++
++      char *error;
++      sql_commit_callback_t *callback;
++      void *context;
++};
++
+ struct event_category event_category_sql = {
+       .name = "sql",
+ };
+@@ -40,6 +63,9 @@ const struct setting_parser_info sql_set
+ struct sql_db_module_register sql_db_module_register = { 0 };
+ ARRAY_TYPE(sql_drivers) sql_drivers;
+ 
++static void sql_query_delayed_callback(struct sql_query_result_delayed *cb);
++static void sql_commit_delayed_callback(struct sql_commit_result_delayed *cb);
++
+ void sql_drivers_init_without_drivers(void)
+ {
+       i_array_init(&sql_drivers, 8);
+@@ -228,9 +254,19 @@ int sql_connect(struct sql_db *db)
+       return db->v.connect(db);
+ }
+ 
++static void sql_call_delayed_callbacks(struct sql_db *db)
++{
++      /* flush any pending results */
++      while (db->query_delayed_list != NULL)
++              sql_query_delayed_callback(db->query_delayed_list);
++      while (db->commit_delayed_list != NULL)
++              sql_commit_delayed_callback(db->commit_delayed_list);
++}
++
+ void sql_disconnect(struct sql_db *db)
+ {
+       timeout_remove(&db->to_reconnect);
++      sql_call_delayed_callbacks(db);
+       db->v.disconnect(db);
+ }
+ 
+@@ -250,11 +286,31 @@ void sql_exec(struct sql_db *db, const c
+       db->v.exec(db, query);
+ }
+ 
++static void sql_query_delayed_callback(struct sql_query_result_delayed *cb)
++{
++      timeout_remove(&cb->to);
++      DLLIST_REMOVE(&cb->db->query_delayed_list, cb);
++      cb->callback(cb->result, cb->context);
++      sql_result_unref(cb->result);
++      i_free(cb);
++}
++
+ #undef sql_query
+ void sql_query(struct sql_db *db, const char *query,
+              sql_query_callback_t *callback, void *context)
+ {
+-      db->v.query(db, query, callback, context);
++      if (db->v.query != NULL) {
++              db->v.query(db, query, callback, context);
++              return;
++      }
++
++      struct sql_query_result_delayed *cb = i_new(struct sql_query_result_delayed, 1);
++      cb->db = db;
++      cb->result = sql_query_s(db, query);
++      cb->callback = callback;
++      cb->context = context;
++      cb->to = timeout_add_short(0, sql_query_delayed_callback, cb);
++      DLLIST_PREPEND(&db->query_delayed_list, cb);
+ }
+ 
+ struct sql_result *sql_query_s(struct sql_db *db, const char *query)
+@@ -494,11 +550,21 @@ void sql_statement_query(struct sql_stat
+                        sql_query_callback_t *callback, void *context)
+ {
+       struct sql_statement *stmt = *_stmt;
+-
+       *_stmt = NULL;
++
+       if (stmt->db->v.statement_query != NULL)
+               stmt->db->v.statement_query(stmt, callback, context);
+-      else
++      else if (stmt->db->v.statement_query_s != NULL) {
++              struct sql_db *db = stmt->db;
++              struct sql_query_result_delayed *cb =
++                      i_new(struct sql_query_result_delayed, 1);
++              cb->db = db;
++              cb->callback = callback;
++              cb->context = context;
++              cb->result = sql_statement_query_s(&stmt);
++              cb->to = timeout_add_short(0, sql_query_delayed_callback, cb);
++              DLLIST_PREPEND(&db->query_delayed_list, cb);
++      } else
+               default_sql_statement_query(stmt, callback, context);
+ }
+ 
+@@ -755,14 +821,40 @@ void sql_transaction_set_non_atomic(stru
+       ctx->non_atomic = TRUE;
+ }
+ 
++static void sql_commit_delayed_callback(struct sql_commit_result_delayed *cb)
++{
++      struct sql_commit_result result = {
++              .error = cb->error,
++      };
++      timeout_remove(&cb->to);
++      DLLIST_REMOVE(&cb->db->commit_delayed_list, cb);
++      cb->callback(&result, cb->context);
++      i_free(cb->error);
++      i_free(cb);
++}
++
+ #undef sql_transaction_commit
+ void sql_transaction_commit(struct sql_transaction_context **_ctx,
+                           sql_commit_callback_t *callback, void *context)
+ {
+       struct sql_transaction_context *ctx = *_ctx;
+-
++      struct sql_db *db = ctx->db;
+       *_ctx = NULL;
+-      ctx->db->v.transaction_commit(ctx, callback, context);
++
++      if (ctx->db->v.transaction_commit != NULL) {
++              ctx->db->v.transaction_commit(ctx, callback, context);
++              return;
++      }
++
++      struct sql_commit_result_delayed *cb = i_new(struct sql_commit_result_delayed, 1);
++      const char *error = NULL;
++      ctx->db->v.transaction_commit_s(ctx, &error);
++      cb->db = db;
++      cb->error = i_strdup(error);
++      cb->callback = callback;
++      cb->context = context;
++      cb->to = timeout_add_short(0, sql_commit_delayed_callback, cb);
++      DLLIST_PREPEND(&db->commit_delayed_list, cb);
+ }
+ 
+ int sql_transaction_commit_s(struct sql_transaction_context **_ctx,
+@@ -900,6 +992,7 @@ void sql_wait(struct sql_db *db)
+ {
+       if (db->v.wait != NULL)
+               db->v.wait(db);
++      sql_call_delayed_callbacks(db);
+ }
+ 
+ 
+Index: trixie/src/lib-sql/sql-api.h
+===================================================================
+--- trixie.orig/src/lib-sql/sql-api.h
++++ trixie/src/lib-sql/sql-api.h
+@@ -123,7 +123,7 @@ const char *sql_escape_blob(struct sql_d
+ void sql_exec(struct sql_db *db, const char *query);
+ /* Execute SQL query and return result in callback. If fields list is given,
+    the returned fields are validated to be of correct type, and you can use
+-   sql_result_next_row_get() */
++   sql_result_next_row_get().  The callback is never called immediately. */
+ void sql_query(struct sql_db *db, const char *query,
+              sql_query_callback_t *callback, void *context);
+ #define sql_query(db, query, callback, context) \
+@@ -229,7 +229,7 @@ struct sql_transaction_context *sql_tran
+ /* Don't require transaction to be atomic. Currently this is implemented only
+    with Cassandra to use UNLOGGED BATCH operations. */
+ void sql_transaction_set_non_atomic(struct sql_transaction_context *ctx);
+-/* Commit transaction. */
++/* Commit transaction. The callback is never called immediately. */
+ void sql_transaction_commit(struct sql_transaction_context **ctx,
+                           sql_commit_callback_t *callback, void *context);
+ #define sql_transaction_commit(ctx, callback, context) \
diff --git a/patches/0001-lib-storage-Allow-NULL-path-in-mailbox_list_mkdir_ro.patch b/patches/0001-lib-storage-Allow-NULL-path-in-mailbox_list_mkdir_ro.patch
new file mode 100644 (file)
index 0000000..8963bd8
--- /dev/null
@@ -0,0 +1,44 @@
+From ca82388f330be27e015ad4b3c08b0156307cf369 Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Tue, 23 Jun 2026 14:59:45 +0000
+Subject: [PATCH 1/2] lib-storage: Allow NULL path in mailbox_list_mkdir_root()
+
+When path is NULL, look up the root directory path of the given type via
+mailbox_list_get_root_path() and create that. If there is no path of the
+requested type, nothing is done. This avoids open-coding the common
+get_root_path() + mkdir_root() pair at call sites.
+---
+ src/lib-storage/mailbox-list.c | 4 ++++
+ src/lib-storage/mailbox-list.h | 4 +++-
+ 2 files changed, 7 insertions(+), 1 deletion(-)
+
+Index: trixie/src/lib-storage/mailbox-list.c
+===================================================================
+--- trixie.orig/src/lib-storage/mailbox-list.c
++++ trixie/src/lib-storage/mailbox-list.c
+@@ -1019,6 +1019,10 @@ int mailbox_list_mkdir_root(struct mailb
+ {
+       const char *error;
+ 
++      if (path == NULL &&
++          !mailbox_list_get_root_path(list, type, &path))
++              return 0;
++
+       if (mailbox_list_try_mkdir_root(list, path, type, &error) < 0) {
+               mailbox_list_set_critical(list, "%s", error);
+               return -1;
+Index: trixie/src/lib-storage/mailbox-list.h
+===================================================================
+--- trixie.orig/src/lib-storage/mailbox-list.h
++++ trixie/src/lib-storage/mailbox-list.h
+@@ -177,7 +177,9 @@ void mailbox_list_get_permissions(struct
+ void mailbox_list_get_root_permissions(struct mailbox_list *list,
+                                      struct mailbox_permissions *permissions_r);
+ /* mkdir() a root directory of given type with proper permissions. The path can
+-   be either the root itself or point to a directory under the root. */
++   be either the root itself or point to a directory under the root. If path is
++   NULL, the root path of the given type is looked up automatically; if there is
++   no such path, nothing is done. */
+ int mailbox_list_mkdir_root(struct mailbox_list *list, const char *path,
+                           enum mailbox_list_path_type type);
+ /* Like mailbox_list_mkdir_root(), but don't log an error if it fails. */
diff --git a/patches/0001-lib-storage-thread-Avoid-excessive-data-stack-growth.patch b/patches/0001-lib-storage-thread-Avoid-excessive-data-stack-growth.patch
new file mode 100644 (file)
index 0000000..5e0fb99
--- /dev/null
@@ -0,0 +1,55 @@
+From 7774d50c731fa97e445fd9f4cb9e4f08e4415f8b Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Mon, 13 Apr 2026 17:28:43 +0200
+Subject: [PATCH 1/3] lib-storage: thread - Avoid excessive data stack growth
+ with many References msgids
+
+---
+ src/lib-storage/index/index-thread.c | 22 +++++++++++++++-------
+ 1 file changed, 15 insertions(+), 7 deletions(-)
+
+diff --git a/src/lib-storage/index/index-thread.c b/src/lib-storage/index/index-thread.c
+index 2b1ba045ac..e20a97072c 100644
+--- a/src/lib-storage/index/index-thread.c
++++ b/src/lib-storage/index/index-thread.c
+@@ -93,9 +93,11 @@ mail_strmap_rec_get_msgid(struct mail_thread_context *ctx,
+ 
+       /* get the nth message-id */
+       msgid = message_id_get_next(&msgids);
+-      if (msgid != NULL) {
+-              for (; n > 0; n--)
++      if (msgid != NULL && n > 0) {
++              for (; n > 1; n--) T_BEGIN {
+                       msgid = message_id_get_next(&msgids);
++              } T_END;
++              msgid = message_id_get_next(&msgids);
+       }
+ 
+       if (msgid == NULL) {
+@@ -266,12 +268,18 @@ mail_thread_map_add_mail(struct mail_thread_context *ctx, struct mail *mail)
+       msgid = message_id_get_next(&references);
+       if (msgid != NULL) {
+               ref_index = MAIL_THREAD_NODE_REF_REFERENCES1;
++              mail_index_strmap_view_sync_add(ctx->strmap_sync,
++                                              mail->uid, ref_index, msgid);
+               do {
+-                      mail_index_strmap_view_sync_add(ctx->strmap_sync,
+-                                                      mail->uid,
+-                                                      ref_index, msgid);
+-                      ref_index++;
+-                      msgid = message_id_get_next(&references);
++                      T_BEGIN {
++                              ref_index++;
++                              msgid = message_id_get_next(&references);
++                              if (msgid != NULL) {
++                                      mail_index_strmap_view_sync_add(
++                                              ctx->strmap_sync, mail->uid,
++                                              ref_index, msgid);
++                              }
++                      } T_END;
+               } while (msgid != NULL);
+       } else {
+               /* no References:, use In-Reply-To: */
+-- 
+2.47.3
+
diff --git a/patches/0001-lib-test-Add-test_assert_memcmp.patch b/patches/0001-lib-test-Add-test_assert_memcmp.patch
new file mode 100644 (file)
index 0000000..75a3029
--- /dev/null
@@ -0,0 +1,104 @@
+From 4677492d038f5187363790b5a93bd5fccc989a98 Mon Sep 17 00:00:00 2001
+From: Stephan Bosch <stephan.bosch@open-xchange.com>
+Date: Fri, 25 Jul 2025 03:31:44 +0200
+Subject: [PATCH] lib-test: Add test_assert_memcmp*()
+
+---
+ src/lib-test/test-common.c | 33 +++++++++++++++++++++++++++++++++
+ src/lib-test/test-common.h | 25 ++++++++++++++++++++++++-
+ 2 files changed, 57 insertions(+), 1 deletion(-)
+
+Index: trixie/src/lib-test/test-common.c
+===================================================================
+--- trixie.orig/src/lib-test/test-common.c
++++ trixie/src/lib-test/test-common.c
+@@ -84,6 +84,39 @@ void test_assert_failed_strcmp_idx(const
+ #endif
+ }
+ 
++static void print_memcmp_data(const void *_data, size_t len)
++{
++      const unsigned char *data = _data;
++      size_t i;
++
++      for (i = 0; i < len; i++) {
++              if (data[i] >= 0x20 && data[i] < 0x7f)
++                      printf("%c", data[i]);
++              else
++                      printf("\\x%02X", data[i]);
++      }
++}
++
++void test_assert_failed_memcmp_idx(const char *code, const char *file, unsigned int line,
++                                 const void *src, const void *dst, size_t len, long long i)
++{
++      printf("%s:%u: Assert", file, line);
++      if (i == LLONG_MIN)
++              printf(" failed: %s\n", code);
++      else
++              printf("(#%lld) failed: %s\n", i, code);
++      printf("        \"");
++      print_memcmp_data(src, len);
++      printf("\" != \"");
++      print_memcmp_data(dst, len);
++      printf("\" (len == %zu)", len);
++      fflush(stdout);
++      test_success = FALSE;
++#ifdef STATIC_CHECKER
++      i_unreached();
++#endif
++}
++
+ void test_assert_failed_cmp_intmax_idx(const char *code, const char *file,
+                                      unsigned int line,
+                                      intmax_t src, intmax_t dst,
+Index: trixie/src/lib-test/test-common.h
+===================================================================
+--- trixie.orig/src/lib-test/test-common.h
++++ trixie/src/lib-test/test-common.h
+@@ -40,7 +40,14 @@ void test_begin(const char *name);
+               test_assert_strcmp_idx(s1, s2, LLONG_MIN); \
+       } STMT_END
+ 
+-/* Same as test_assert_strcmp expect that it takes an additional i as input.
++/* Additional parameters are m1 (source) and m2 (destination) memory and len
++ * in memcmp().
++ */
++#define test_assert_memcmp(m1, m2, len) STMT_START { \
++              test_assert_memcmp_idx(m1, m2, len, LLONG_MIN); \
++      } STMT_END
++
++/* Same as test_assert_strcmp except that it takes an additional i as input.
+  * When i is greater than or equals 0 it is used to identify the barrage of
+  * tests failed like in test_assert_idx.
+ */
+@@ -52,6 +59,19 @@ void test_begin(const char *name);
+                                                     __FILE__, __LINE__, _temp_s1, _temp_s2, i); \
+       } STMT_END
+ 
++/* Same as test_assert_memcmp except that it takes an additional i as input.
++ * When i is greater than or equals 0 it is used to identify the barrage of
++ * tests failed like in test_assert_idx.
++*/
++#define test_assert_memcmp_idx(_m1, _m2, _len, i) STMT_START { \
++              const void *_temp_m1 = (_m1); \
++              const void *_temp_m2 = (_m2); \
++              const size_t _temp_len = (_len); \
++              if ((memcmp(_temp_m1,_temp_m2, _temp_len) != 0)) \
++                      test_assert_failed_memcmp_idx("memcmp(" #_m1 ","  #_m2 ","  #_len ")", \
++                                                    __FILE__, __LINE__, _temp_m1, _temp_m2, _temp_len, i); \
++      } STMT_END
++
+ #define test_assert_cmp_bool(_bool_value1, _op, _value2) \
+       test_assert_cmp((unsigned int) _bool_value1, _op, (unsigned int _bool_value2))
+ 
+@@ -92,6 +112,9 @@ void test_assert_failed_idx(const char *
+ void test_assert_failed_strcmp_idx(const char *code, const char *file, unsigned int line,
+                                  const char * src, const char * dst, long long i)
+       ATTR_STATIC_CHECKER_NORETURN;
++void test_assert_failed_memcmp_idx(const char *code, const char *file, unsigned int line,
++                                 const void *src, const void *dst, size_t len, long long i)
++      ATTR_STATIC_CHECKER_NORETURN;
+ void test_assert_failed_cmp_intmax_idx(const char *code, const char *file,
+                                      unsigned int line,
+                                      intmax_t src, intmax_t dst,
diff --git a/patches/0001-lib-test-Require-both-lengths-in-test_assert_memcmp.patch b/patches/0001-lib-test-Require-both-lengths-in-test_assert_memcmp.patch
new file mode 100644 (file)
index 0000000..8dbd82c
--- /dev/null
@@ -0,0 +1,45 @@
+From 8f04979a7e72d3b3e04f3a93bd8088f5e2332dd9 Mon Sep 17 00:00:00 2001
+From: Aki Tuomi <aki.tuomi@open-xchange.com>
+Date: Tue, 27 Jan 2026 19:02:30 +0200
+Subject: [PATCH] lib-test: Require both lengths in test_assert_memcmp()
+
+---
+ src/lib-sql/test-sql-sqlite.c |  2 +-
+ src/lib-test/test-common.h    | 14 ++++++++------
+ src/lib/test-unicode-nf.c     | 16 ++++++++--------
+ 3 files changed, 17 insertions(+), 15 deletions(-)
+
+Index: trixie/src/lib-test/test-common.h
+===================================================================
+--- trixie.orig/src/lib-test/test-common.h
++++ trixie/src/lib-test/test-common.h
+@@ -43,8 +43,8 @@ void test_begin(const char *name);
+ /* Additional parameters are m1 (source) and m2 (destination) memory and len
+  * in memcmp().
+  */
+-#define test_assert_memcmp(m1, m2, len) STMT_START { \
+-              test_assert_memcmp_idx(m1, m2, len, LLONG_MIN); \
++#define test_assert_memcmp(m1, len1, m2, len2) STMT_START { \
++              test_assert_memcmp_idx(m1, len1, m2, len2, LLONG_MIN); \
+       } STMT_END
+ 
+ /* Same as test_assert_strcmp except that it takes an additional i as input.
+@@ -63,12 +63,14 @@ void test_begin(const char *name);
+  * When i is greater than or equals 0 it is used to identify the barrage of
+  * tests failed like in test_assert_idx.
+ */
+-#define test_assert_memcmp_idx(_m1, _m2, _len, i) STMT_START { \
++#define test_assert_memcmp_idx(_m1, _len1, _m2, _len2, i) STMT_START { \
+               const void *_temp_m1 = (_m1); \
+               const void *_temp_m2 = (_m2); \
+-              const size_t _temp_len = (_len); \
+-              if ((memcmp(_temp_m1,_temp_m2, _temp_len) != 0)) \
+-                      test_assert_failed_memcmp_idx("memcmp(" #_m1 ","  #_m2 ","  #_len ")", \
++              const size_t _temp_len = I_MIN((_len1), (_len2)); \
++              if ((_len1) != (_len2)) \
++                      test_assert_failed_ucmp_intmax_idx(#_len1 " == " #_len2, __FILE__, __LINE__, _len1, _len2, "=", i); \
++              if ((memcmp(_temp_m1, _temp_m2, _temp_len) != 0)) \
++                      test_assert_failed_memcmp_idx("memcmp(" #_m1 ","  #_m2 ","  #_len2 ")", \
+                                                     __FILE__, __LINE__, _temp_m1, _temp_m2, _temp_len, i); \
+       } STMT_END
+ 
diff --git a/patches/0001-lib-xxh64-fix-byte-ordering-bug-on-big-endian-system.patch b/patches/0001-lib-xxh64-fix-byte-ordering-bug-on-big-endian-system.patch
new file mode 100644 (file)
index 0000000..0c31dbc
--- /dev/null
@@ -0,0 +1,45 @@
+From 45406e13f1faddf341922ff73962820bc0a99015 Mon Sep 17 00:00:00 2001
+From: Noah Meyerhans <noahm@debian.org>
+Date: Tue, 1 Sep 2026 21:12:51 -0400
+Subject: [PATCH] lib: xxh64: fix byte ordering bug on big-endian systems
+
+Convert from little-endian to host byte order where necessary.
+
+Background at https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1146449
+---
+ src/lib/xxh64.c | 6 ++++--
+ 1 file changed, 4 insertions(+), 2 deletions(-)
+
+diff --git a/src/lib/xxh64.c b/src/lib/xxh64.c
+index 1c17833afd..4a151bd0c2 100644
+--- a/src/lib/xxh64.c
++++ b/src/lib/xxh64.c
+@@ -8,6 +8,8 @@
+ #include "lib.h"
+ #include "xxh64.h"
+ 
++#include <endian.h>
++
+ #define XXH64_PRIME1  UINT64_C(0x9E3779B185EBCA87)
+ #define XXH64_PRIME2  UINT64_C(0xC2B2AE3D27D4EB4F)
+ #define XXH64_PRIME3  UINT64_C(0x165667B19E3779F9)
+@@ -20,14 +22,14 @@ static inline uint64_t xxh64_read64(const void *p)
+ {
+       uint64_t v;
+       memcpy(&v, p, sizeof(v));
+-      return v;
++      return le64toh(v);  /* Convert from little-endian to host byte order */
+ }
+ 
+ static inline uint32_t xxh64_read32(const void *p)
+ {
+       uint32_t v;
+       memcpy(&v, p, sizeof(v));
+-      return v;
++      return le32toh(v);  /* Convert from little-endian to host byte order */
+ }
+ 
+ static uint64_t ATTR_UNSIGNED_WRAPS xxh64_round(uint64_t acc, uint64_t input)
+-- 
+2.47.3
+
diff --git a/patches/0001-login-Add-LOGIN_PROXY_FAILURE_TYPE_AUTH_NOT_REPLIED.patch b/patches/0001-login-Add-LOGIN_PROXY_FAILURE_TYPE_AUTH_NOT_REPLIED.patch
new file mode 100644 (file)
index 0000000..c617355
--- /dev/null
@@ -0,0 +1,99 @@
+From 3eed5e02c49c2dc1522c038869f723e7c6d93ffc Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Fri, 18 Jul 2025 14:28:47 +0300
+Subject: [PATCH] *-login: Add LOGIN_PROXY_FAILURE_TYPE_AUTH_NOT_REPLIED
+
+---
+ src/imap-login/imap-proxy.c             | 1 +
+ src/login-common/client-common.c        | 1 +
+ src/login-common/login-proxy.c          | 2 ++
+ src/login-common/login-proxy.h          | 3 +++
+ src/pop3-login/pop3-proxy.c             | 1 +
+ src/submission-login/submission-proxy.c | 1 +
+ 6 files changed, 9 insertions(+)
+
+diff --git a/src/imap-login/imap-proxy.c b/src/imap-login/imap-proxy.c
+index 69680ea752..03dad690e1 100644
+--- a/src/imap-login/imap-proxy.c
++++ b/src/imap-login/imap-proxy.c
+@@ -586,6 +586,7 @@ imap_proxy_send_failure_reply(struct imap_client *imap_client,
+               break;
+       case LOGIN_PROXY_FAILURE_TYPE_REMOTE_CONFIG:
+       case LOGIN_PROXY_FAILURE_TYPE_INTERNAL_CONFIG:
++      case LOGIN_PROXY_FAILURE_TYPE_AUTH_NOT_REPLIED:
+               client_send_reply_code(&imap_client->common, IMAP_CMD_REPLY_NO,
+                                      IMAP_RESP_CODE_SERVERBUG,
+                                      LOGIN_PROXY_FAILURE_MSG);
+diff --git a/src/login-common/client-common.c b/src/login-common/client-common.c
+index 0c11a925f1..eeb9d6da05 100644
+--- a/src/login-common/client-common.c
++++ b/src/login-common/client-common.c
+@@ -1386,6 +1386,7 @@ bool client_get_extra_disconnect_reason(struct client *client,
+                       last_reason = "protocol failure";
+                       break;
+               case LOGIN_PROXY_FAILURE_TYPE_AUTH_REPLIED:
++              case LOGIN_PROXY_FAILURE_TYPE_AUTH_NOT_REPLIED:
+                       event_reason = "auth_failed";
+                       last_reason = "authentication failure";
+                       break;
+diff --git a/src/login-common/login-proxy.c b/src/login-common/login-proxy.c
+index 0e3e8c9be2..10c07b711d 100644
+--- a/src/login-common/login-proxy.c
++++ b/src/login-common/login-proxy.c
+@@ -830,6 +830,7 @@ bool login_proxy_failed(struct login_proxy *proxy, struct event *event,
+               log_prefix = "Remote server sent invalid input: ";
+               break;
+       case LOGIN_PROXY_FAILURE_TYPE_AUTH_REPLIED:
++      case LOGIN_PROXY_FAILURE_TYPE_AUTH_NOT_REPLIED:
+       case LOGIN_PROXY_FAILURE_TYPE_AUTH_LIMIT_REACHED_REPLIED:
+               log_prefix = "";
+               try_reconnect = FALSE;
+@@ -857,6 +858,7 @@ bool login_proxy_failed(struct login_proxy *proxy, struct event *event,
+ 
+       if (type != LOGIN_PROXY_FAILURE_TYPE_AUTH_REPLIED &&
+           type != LOGIN_PROXY_FAILURE_TYPE_AUTH_LIMIT_REACHED_REPLIED &&
++          type != LOGIN_PROXY_FAILURE_TYPE_AUTH_NOT_REPLIED &&
+           type != LOGIN_PROXY_FAILURE_TYPE_AUTH_TEMPFAIL)
+               e_error(event, "%s%s", log_prefix, reason);
+       else if (proxy->client->set->auth_verbose)
+diff --git a/src/login-common/login-proxy.h b/src/login-common/login-proxy.h
+index 65895f3d75..736c2eb586 100644
+--- a/src/login-common/login-proxy.h
++++ b/src/login-common/login-proxy.h
+@@ -35,6 +35,9 @@ enum login_proxy_failure_type {
+       /* Authentication failed to backend. The LOGIN/AUTH command reply was
+          already sent to the client. */
+       LOGIN_PROXY_FAILURE_TYPE_AUTH_REPLIED,
++      /* Authentication failed to backend. The LOGIN/AUTH command reply was
++         NOT sent to the client yet. */
++      LOGIN_PROXY_FAILURE_TYPE_AUTH_NOT_REPLIED,
+       /* Authentication failed with a temporary failure code. Attempting it
+          again might work. */
+       LOGIN_PROXY_FAILURE_TYPE_AUTH_TEMPFAIL,
+diff --git a/src/pop3-login/pop3-proxy.c b/src/pop3-login/pop3-proxy.c
+index f8010d3d12..ad7786903b 100644
+--- a/src/pop3-login/pop3-proxy.c
++++ b/src/pop3-login/pop3-proxy.c
+@@ -368,6 +368,7 @@ pop3_proxy_send_failure_reply(struct client *client,
+               break;
+       case LOGIN_PROXY_FAILURE_TYPE_INTERNAL_CONFIG:
+       case LOGIN_PROXY_FAILURE_TYPE_REMOTE_CONFIG:
++      case LOGIN_PROXY_FAILURE_TYPE_AUTH_NOT_REPLIED:
+               client_send_reply(client, POP3_CMD_REPLY_ERROR,
+                                 LOGIN_PROXY_FAILURE_MSG);
+               break;
+diff --git a/src/submission-login/submission-proxy.c b/src/submission-login/submission-proxy.c
+index 6f2c7f436a..f86845dcbc 100644
+--- a/src/submission-login/submission-proxy.c
++++ b/src/submission-login/submission-proxy.c
+@@ -752,6 +752,7 @@ submission_proxy_send_failure_reply(struct submission_client *subm_client,
+       case LOGIN_PROXY_FAILURE_TYPE_REMOTE_CONFIG:
+       case LOGIN_PROXY_FAILURE_TYPE_PROTOCOL:
+       case LOGIN_PROXY_FAILURE_TYPE_AUTH_REDIRECT:
++      case LOGIN_PROXY_FAILURE_TYPE_AUTH_NOT_REPLIED:
+               i_assert(cmd != NULL);
+               subm_client->pending_auth = NULL;
+               smtp_server_reply(cmd, 454, "4.7.0", LOGIN_PROXY_FAILURE_MSG);
+-- 
+2.47.3
+
diff --git a/patches/0001-login-common-Fix-crash-when-XCLIENT-FORWARD-base64-c.patch b/patches/0001-login-common-Fix-crash-when-XCLIENT-FORWARD-base64-c.patch
new file mode 100644 (file)
index 0000000..9291f25
--- /dev/null
@@ -0,0 +1,42 @@
+From 7c874f2d6461e80ff8f7111fa36a7551747129f1 Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Sun, 3 May 2026 16:20:00 +0000
+Subject: [PATCH 1/2] login-common: Fix crash when XCLIENT FORWARD= base64
+ contains NUL byte
+
+Sending "XCLIENT FORWARD=AA==" (base64 for a single NUL byte) followed
+by a regular login crashed pop3-login (and other login services) with:
+
+  Panic: file ../../src/lib/array.h: line 275 (array_idx_i):
+  assertion failed: (idx < array->buffer->used / array->element_size)
+
+p_strsplit_tabescaped() truncates at the first NUL, so an all-NUL
+payload produced an empty fields list. forward_fields was still
+created (but empty), and the later array_front() call in
+sasl_server_auth_begin() then panicked on the empty array.
+
+Reject empty payloads and payloads containing NUL bytes during base64
+decode, so the array is never created in this case.
+---
+ src/login-common/client-common.c | 5 +++++
+ 1 file changed, 5 insertions(+)
+
+diff --git a/src/login-common/client-common.c b/src/login-common/client-common.c
+index eeb9d6da05..00e4f9abbf 100644
+--- a/src/login-common/client-common.c
++++ b/src/login-common/client-common.c
+@@ -988,6 +988,11 @@ bool client_forward_decode_base64(struct client *client, const char *value)
+       string_t *str = t_str_new(MAX_BASE64_DECODED_SIZE(value_len));
+       if (base64_decode(value, value_len, str) < 0)
+               return FALSE;
++      /* Embedded NUL would yield a created-but-empty array, panicking later
++         in array_front() during sasl_server_auth_begin(). */
++      if (str_len(str) == 0 ||
++          memchr(str_data(str), '\0', str_len(str)) != NULL)
++              return FALSE;
+ 
+       char **_fields = p_strsplit_tabescaped(client->preproxy_pool,
+                                              str_c(str));
+-- 
+2.47.3
+
diff --git a/patches/0001-login-common-client-common-Add-client_disconnect-vfu.patch b/patches/0001-login-common-client-common-Add-client_disconnect-vfu.patch
new file mode 100644 (file)
index 0000000..f0bf5c1
--- /dev/null
@@ -0,0 +1,36 @@
+From 275331da2aca21fb75483f226e72b0890d43ede4 Mon Sep 17 00:00:00 2001
+From: Stephan Bosch <stephan.bosch@open-xchange.com>
+Date: Fri, 28 Nov 2025 03:32:36 +0100
+Subject: [PATCH 1/5] login-common: client-common - Add client_disconnect()
+ vfunc
+
+---
+ src/login-common/client-common.c | 2 ++
+ src/login-common/client-common.h | 1 +
+ 2 files changed, 3 insertions(+)
+
+Index: trixie/src/login-common/client-common.c
+===================================================================
+--- trixie.orig/src/login-common/client-common.c
++++ trixie/src/login-common/client-common.c
+@@ -433,6 +433,8 @@ void client_disconnect(struct client *cl
+       if (!client->login_success) {
+               bool unref = FALSE;
+ 
++              if (client->v.disconnect != NULL)
++                      client->v.disconnect(client, reason);
+               io_remove(&client->io);
+               ssl_iostream_destroy(&client->ssl_iostream);
+               if (client->iostream_fd_proxy != NULL) {
+Index: trixie/src/login-common/client-common.h
+===================================================================
+--- trixie.orig/src/login-common/client-common.h
++++ trixie/src/login-common/client-common.h
+@@ -122,6 +122,7 @@ struct client_auth_reply {
+ struct client_vfuncs {
+       struct client *(*alloc)(pool_t pool);
+       int (*create)(struct client *client);
++      void (*disconnect)(struct client *client, const char *reason);
+       void (*destroy)(struct client *client);
+       int (*reload_config)(struct client *client, const char **error_r);
+       void (*notify_auth_ready)(struct client *client);
diff --git a/patches/0001-login-common-login-Add-proxy_dest_connection_limit-e.patch b/patches/0001-login-common-login-Add-proxy_dest_connection_limit-e.patch
new file mode 100644 (file)
index 0000000..c056df0
--- /dev/null
@@ -0,0 +1,134 @@
+From db0835c84bf754a260ce0d3dca1d69eb90f87518 Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Wed, 3 Dec 2025 12:06:42 +0200
+Subject: [PATCH] login-common, *-login: Add proxy_dest_connection_limit
+ error_code to proxy_session_finished
+
+If IMAP backend returns with [LIMIT] or POP3 backend returns with [IN-USE],
+use this error code rather than the generic proxy_dest_auth_failed.
+Error messages are also updated.
+---
+ src/imap-login/imap-proxy.c             | 9 +++++++++
+ src/login-common/client-common.c        | 4 ++++
+ src/login-common/login-proxy.c          | 2 ++
+ src/login-common/login-proxy.h          | 3 +++
+ src/pop3-login/pop3-proxy.c             | 3 +++
+ src/submission-login/submission-proxy.c | 1 +
+ 6 files changed, 22 insertions(+)
+
+Index: dovecot/src/imap-login/imap-proxy.c
+===================================================================
+--- dovecot.orig/src/imap-login/imap-proxy.c
++++ dovecot/src/imap-login/imap-proxy.c
+@@ -284,6 +284,12 @@ static bool auth_resp_code_is_serverbug(
+                          strlen(IMAP_RESP_CODE_SERVERBUG"]")) == 0;
+ }
+ 
++static bool auth_resp_code_is_limit(const char *resp)
++{
++      return strncasecmp(resp, IMAP_RESP_CODE_LIMIT"]",
++                         strlen(IMAP_RESP_CODE_LIMIT"]")) == 0;
++}
++
+ static bool
+ auth_resp_code_parse_referral(struct client *client, const char *resp,
+                             const char **userhostport_r)
+@@ -448,6 +454,8 @@ int imap_proxy_parse_line(struct client
+                       else if (auth_resp_code_is_serverbug(line + 4))
+                               failure_type = LOGIN_PROXY_FAILURE_TYPE_REMOTE;
+                       else {
++                              if (auth_resp_code_is_limit(line + 4))
++                                      failure_type = LOGIN_PROXY_FAILURE_TYPE_AUTH_LIMIT_REACHED_REPLIED;
+                               client_send_raw(client, t_strconcat(
+                                       imap_client->cmd_tag, " ", line, "\r\n", NULL));
+                       }
+@@ -587,6 +595,7 @@ imap_proxy_send_failure_reply(struct ima
+                       imap_client->cmd_tag, " NO ", reason, "\r\n", NULL));
+               break;
+       case LOGIN_PROXY_FAILURE_TYPE_AUTH_REPLIED:
++      case LOGIN_PROXY_FAILURE_TYPE_AUTH_LIMIT_REACHED_REPLIED:
+               /* reply was already sent */
+               break;
+       }
+Index: dovecot/src/login-common/client-common.c
+===================================================================
+--- dovecot.orig/src/login-common/client-common.c
++++ dovecot/src/login-common/client-common.c
+@@ -1397,6 +1397,10 @@ bool client_get_extra_disconnect_reason(
+                       event_reason = "redirected";
+                       last_reason = "redirected";
+                       break;
++              case LOGIN_PROXY_FAILURE_TYPE_AUTH_LIMIT_REACHED_REPLIED:
++                      event_reason = "connection_limit";
++                      last_reason = "connection limit reached";
++                      break;
+               default:
+                       i_unreached();
+               }
+Index: dovecot/src/login-common/login-proxy.c
+===================================================================
+--- dovecot.orig/src/login-common/login-proxy.c
++++ dovecot/src/login-common/login-proxy.c
+@@ -830,6 +830,7 @@ bool login_proxy_failed(struct login_pro
+               log_prefix = "Remote server sent invalid input: ";
+               break;
+       case LOGIN_PROXY_FAILURE_TYPE_AUTH_REPLIED:
++      case LOGIN_PROXY_FAILURE_TYPE_AUTH_LIMIT_REACHED_REPLIED:
+               log_prefix = "";
+               try_reconnect = FALSE;
+               break;
+@@ -855,6 +856,7 @@ bool login_proxy_failed(struct login_pro
+       }
+ 
+       if (type != LOGIN_PROXY_FAILURE_TYPE_AUTH_REPLIED &&
++          type != LOGIN_PROXY_FAILURE_TYPE_AUTH_LIMIT_REACHED_REPLIED &&
+           type != LOGIN_PROXY_FAILURE_TYPE_AUTH_TEMPFAIL)
+               e_error(event, "%s%s", log_prefix, reason);
+       else if (proxy->client->set->auth_verbose)
+Index: dovecot/src/login-common/login-proxy.h
+===================================================================
+--- dovecot.orig/src/login-common/login-proxy.h
++++ dovecot/src/login-common/login-proxy.h
+@@ -41,6 +41,9 @@ enum login_proxy_failure_type {
+       /* Authentication requests connecting to another host. The reason
+          string contains the host (and optionally :port). */
+       LOGIN_PROXY_FAILURE_TYPE_AUTH_REDIRECT,
++      /* Authentication failed because user has reached some limit.
++         The LOGIN/AUTH command reply was already sent to the client. */
++      LOGIN_PROXY_FAILURE_TYPE_AUTH_LIMIT_REACHED_REPLIED,
+ };
+ 
+ struct login_proxy_settings {
+Index: dovecot/src/pop3-login/pop3-proxy.c
+===================================================================
+--- dovecot.orig/src/pop3-login/pop3-proxy.c
++++ dovecot/src/pop3-login/pop3-proxy.c
+@@ -333,6 +333,8 @@ int pop3_proxy_parse_line(struct client
+       } else if (pop3_proxy_parse_referral(client, line + 5, &line)) {
+               failure_type = LOGIN_PROXY_FAILURE_TYPE_AUTH_REDIRECT;
+       } else {
++              if (str_begins_with(line, "-ERR [IN-USE]"))
++                      failure_type = LOGIN_PROXY_FAILURE_TYPE_AUTH_LIMIT_REACHED_REPLIED;
+               client_send_raw(client, t_strconcat(line, "\r\n", NULL));
+               line += 5;
+       }
+@@ -374,6 +376,7 @@ pop3_proxy_send_failure_reply(struct cli
+               client_send_reply(client, POP3_CMD_REPLY_ERROR, reason);
+               break;
+       case LOGIN_PROXY_FAILURE_TYPE_AUTH_REPLIED:
++      case LOGIN_PROXY_FAILURE_TYPE_AUTH_LIMIT_REACHED_REPLIED:
+               /* reply was already sent */
+               break;
+       }
+Index: dovecot/src/submission-login/submission-proxy.c
+===================================================================
+--- dovecot.orig/src/submission-login/submission-proxy.c
++++ dovecot/src/submission-login/submission-proxy.c
+@@ -750,6 +750,7 @@ submission_proxy_send_failure_reply(stru
+               smtp_server_reply_submit(subm_client->proxy_reply);
+               break;
+       case LOGIN_PROXY_FAILURE_TYPE_AUTH_REPLIED:
++      case LOGIN_PROXY_FAILURE_TYPE_AUTH_LIMIT_REACHED_REPLIED:
+               /* reply was already sent */
+               i_assert(cmd == NULL);
+               break;
diff --git a/patches/0001-managesieve-login-Adjust-to-core-s-login_proxy_failu.patch b/patches/0001-managesieve-login-Adjust-to-core-s-login_proxy_failu.patch
new file mode 100644 (file)
index 0000000..53d4a57
--- /dev/null
@@ -0,0 +1,30 @@
+Index: dovecot/pigeonhole/src/managesieve-login/managesieve-proxy.c
+===================================================================
+--- dovecot.orig/pigeonhole/src/managesieve-login/managesieve-proxy.c
++++ dovecot/pigeonhole/src/managesieve-login/managesieve-proxy.c
+@@ -658,7 +658,7 @@ int managesieve_proxy_parse_line(struct
+                                                      &reason))
+                       failure_type = LOGIN_PROXY_FAILURE_TYPE_AUTH_REDIRECT;
+               else {
+-                      failure_type = LOGIN_PROXY_FAILURE_TYPE_AUTH;
++                      failure_type = LOGIN_PROXY_FAILURE_TYPE_AUTH_REPLIED;
+                       client_send_no(client, AUTH_FAILED_MSG);
+               }
+ 
+@@ -702,6 +702,7 @@ managesieve_proxy_send_failure_reply(str
+               break;
+       case LOGIN_PROXY_FAILURE_TYPE_INTERNAL_CONFIG:
+       case LOGIN_PROXY_FAILURE_TYPE_REMOTE_CONFIG:
++      case LOGIN_PROXY_FAILURE_TYPE_AUTH_NOT_REPLIED:
+               client_send_reply_code(client, MANAGESIEVE_CMD_REPLY_NO,
+                                      NULL, LOGIN_PROXY_FAILURE_MSG);
+               break;
+@@ -709,7 +710,7 @@ managesieve_proxy_send_failure_reply(str
+               client_send_reply_code(client, MANAGESIEVE_CMD_REPLY_NO,
+                                      "TRYLATER", reason);
+               break;
+-      case LOGIN_PROXY_FAILURE_TYPE_AUTH:
++      case LOGIN_PROXY_FAILURE_TYPE_AUTH_REPLIED:
+               /* reply was already sent */
+               break;
+       }
diff --git a/patches/0001-managesieve-login-Support-LOGIN_PROXY_FAILURE_TYPE_A.patch b/patches/0001-managesieve-login-Support-LOGIN_PROXY_FAILURE_TYPE_A.patch
new file mode 100644 (file)
index 0000000..78c4e12
--- /dev/null
@@ -0,0 +1,12 @@
+Index: dovecot/pigeonhole/src/managesieve-login/managesieve-proxy.c
+===================================================================
+--- dovecot.orig/pigeonhole/src/managesieve-login/managesieve-proxy.c
++++ dovecot/pigeonhole/src/managesieve-login/managesieve-proxy.c
+@@ -711,6 +711,7 @@ managesieve_proxy_send_failure_reply(str
+                                      "TRYLATER", reason);
+               break;
+       case LOGIN_PROXY_FAILURE_TYPE_AUTH_REPLIED:
++      case LOGIN_PROXY_FAILURE_TYPE_AUTH_LIMIT_REACHED_REPLIED:
+               /* reply was already sent */
+               break;
+       }
diff --git a/patches/0001-submission-Fix-settings-leak-on-error-paths-in-clien.patch b/patches/0001-submission-Fix-settings-leak-on-error-paths-in-clien.patch
new file mode 100644 (file)
index 0000000..de0fc9a
--- /dev/null
@@ -0,0 +1,33 @@
+From 53abb9f06e9d81ea41e30c6552d0ff0047e41e9d Mon Sep 17 00:00:00 2001
+From: Markus Valentin <markus.valentin@open-xchange.com>
+Date: Thu, 23 Apr 2026 12:53:07 +0200
+Subject: [PATCH 1/2] submission: Fix settings leak on error paths in
+ client_create_from_input()
+
+---
+ src/submission/main.c | 2 ++
+ 1 file changed, 2 insertions(+)
+
+diff --git a/src/submission/main.c b/src/submission/main.c
+index 00e6642510..3b110f4f4a 100644
+--- a/src/submission/main.c
++++ b/src/submission/main.c
+@@ -208,6 +208,7 @@ client_create_from_input(const struct mail_storage_service_input *input,
+                       "(submission_relay_host is unset)";
+               send_error(fd_out, event, set->hostname,
+                          "4.3.5", MAIL_ERRSTR_CRITICAL_MSG);
++              settings_free(set);
+               mail_user_deinit(&mail_user);
+               event_unref(&event);
+               return -1;
+@@ -216,6 +217,7 @@ client_create_from_input(const struct mail_storage_service_input *input,
+       if (ret < 0) {
+               send_error(fd_out, event, my_hostname,
+                       "4.7.0", MAIL_ERRSTR_CRITICAL_MSG);
++              settings_free(set);
+               mail_user_deinit(&mail_user);
+               event_unref(&event);
+               return -1;
+-- 
+2.47.3
+
diff --git a/patches/0001-trash-Use-mailbox-event-in-trash_try_mailbox-for-set.patch b/patches/0001-trash-Use-mailbox-event-in-trash_try_mailbox-for-set.patch
new file mode 100644 (file)
index 0000000..10683b0
--- /dev/null
@@ -0,0 +1,63 @@
+From 06af53902479572fc96f04b4372fdabb9d01996b Mon Sep 17 00:00:00 2001
+From: Aki Tuomi <aki.tuomi@open-xchange.com>
+Date: Fri, 9 Jan 2026 13:31:42 +0200
+Subject: [PATCH] trash: Use mailbox event in trash_try_mailbox() for settings
+Bug-Debian: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1127029
+
+---
+ src/plugins/trash/trash-plugin.c | 18 ++++++++++--------
+ 1 file changed, 10 insertions(+), 8 deletions(-)
+
+diff --git a/src/plugins/trash/trash-plugin.c b/src/plugins/trash/trash-plugin.c
+index 827ba91ec5..7865de2ef6 100644
+--- a/src/plugins/trash/trash-plugin.c
++++ b/src/plugins/trash/trash-plugin.c
+@@ -570,14 +570,13 @@ static int trash_mailbox_priority_cmp(const struct trash_mailbox *t1,
+       return strcmp(t1->name, t2->name);
+ }
+ 
+-static int trash_try_mailbox(struct mail_namespace *ns, const char *box_name,
++static int trash_try_mailbox(struct mail_namespace *ns, struct event *mbox_event,
+                            const char **error_r)
+ {
+       struct trash_user *tuser = TRASH_USER_CONTEXT_REQUIRE(ns->user);
+       const struct trash_settings *trash_set;
+-      if (settings_try_get_filter(ns->list->event, "mailbox", box_name,
+-                                  &trash_setting_parser_info, 0,
+-                                  &trash_set, error_r) < 0)
++      if (settings_get(mbox_event, &trash_setting_parser_info, 0,
++                       &trash_set, error_r) < 0)
+               return -1;
+       unsigned int trash_priority = trash_set->trash_priority;
+       settings_free(trash_set);
+@@ -586,9 +585,8 @@ static int trash_try_mailbox(struct mail_namespace *ns, const char *box_name,
+               return 0;
+ 
+       const struct mailbox_settings *box_set;
+-      if (settings_try_get_filter(ns->list->event, "mailbox", box_name,
+-                                  &mailbox_setting_parser_info, 0,
+-                                  &box_set, error_r) < 0)
++      if (settings_get(mbox_event, &mailbox_setting_parser_info, 0,
++                       &box_set, error_r) < 0)
+               return -1;
+ 
+       const char *vname =
+@@ -618,10 +616,14 @@ static int trash_find_mailboxes(struct mail_user *user)
+                       continue;
+ 
+               array_foreach_elem(&ns->set->mailboxes, box_name) {
+-                      if (trash_try_mailbox(ns, box_name, &error) < 0) {
++                      struct event *mbox_event =
++                              mail_storage_mailbox_create_event(ns->list->event, ns->list, box_name);
++                      if (trash_try_mailbox(ns, mbox_event, &error) < 0) {
+                               user->error = p_strdup(user->pool, error);
++                              event_unref(&mbox_event);
+                               return -1;
+                       }
++                      event_unref(&mbox_event);
+               }
+       }
+ 
+-- 
+2.47.3
+
diff --git a/patches/0002-auth-Fix-prefixing-forward_fields-without-a-value-fr.patch b/patches/0002-auth-Fix-prefixing-forward_fields-without-a-value-fr.patch
new file mode 100644 (file)
index 0000000..a69e3cc
--- /dev/null
@@ -0,0 +1,30 @@
+From ed04e392f97a523dd7bc75161af280df6cf3ce6f Mon Sep 17 00:00:00 2001
+From: Markus Valentin <markus.valentin@open-xchange.com>
+Date: Thu, 23 Apr 2026 13:07:08 +0200
+Subject: [PATCH 2/2] auth: Fix prefixing forward_fields without a value from
+ client
+
+Bare tokens (without '=') were not prefixed, only key=value pairs were.
+In practice this affected forward_fields, where a bare token such as
+'nopassword' would land in extra_fields unprefixed instead of as
+'forward_nopassword', allowing injection of internal auth control fields.
+---
+ src/auth/auth-fields.c | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+diff --git a/src/auth/auth-fields.c b/src/auth/auth-fields.c
+index 8b92836588..6e1ccbbcab 100644
+--- a/src/auth/auth-fields.c
++++ b/src/auth/auth-fields.c
+@@ -125,7 +125,7 @@ static void auth_fields_import_prefixed_args(struct auth_fields *fields,
+       for (; *args != NULL; args++) {
+               value = strchr(*args, '=');
+               if (value == NULL) {
+-                      key = *args;
++                      key = *prefix != '\0' ? t_strconcat(prefix, *args, NULL) : *args;
+               } else {
+                       key = t_strdup_until(*args, value++);
+                       if (*prefix != '\0')
+-- 
+2.47.3
+
diff --git a/patches/0002-auth-db-oauth2-Fix-scope-check-to-require-all-config.patch b/patches/0002-auth-db-oauth2-Fix-scope-check-to-require-all-config.patch
new file mode 100644 (file)
index 0000000..93d0c1f
--- /dev/null
@@ -0,0 +1,49 @@
+From 5b894458088293ffaef32d028af81a70506b7f20 Mon Sep 17 00:00:00 2001
+From: Aki Tuomi <aki.tuomi@open-xchange.com>
+Date: Wed, 3 Jun 2026 12:56:20 +0000
+Subject: [PATCH 2/4] auth: db-oauth2: Fix scope check to require all
+ configured scopes
+
+Switch token-in-scope check to AND semantics: all configured scopes
+must be present in the token. Behaviour now matches the JWT path.
+---
+ src/auth/db-oauth2.c | 17 ++++++++++-------
+ 1 file changed, 10 insertions(+), 7 deletions(-)
+
+Index: trixie/src/auth/db-oauth2.c
+===================================================================
+--- trixie.orig/src/auth/db-oauth2.c
++++ trixie/src/auth/db-oauth2.c
+@@ -556,22 +556,25 @@ db_oauth2_token_in_scope(struct db_oauth
+       if (array_is_empty(&req->db->set->scope))
+               return TRUE;
+ 
+-      bool found = FALSE;
+       const char *value = auth_fields_find(req->fields, "scope");
+       bool has_scope = value != NULL;
+       if (!has_scope)
+               value = auth_fields_find(req->fields, "aud");
+       e_debug(authdb_event(req->auth_request),
+-              "Token scope(s): %s",
+-              value);
+-      if (value != NULL) {
+-              const char *wanted_scope;
++              "Token scope(s): %s", value);
++
++      bool found = FALSE;
++      if (value != NULL && *value != '\0') {
+               const char *const *entries = has_scope ?
+                       t_strsplit_spaces(value, " ") :
+                       t_strsplit_tabescaped(value);
+-              array_foreach_elem(&req->db->set->scope, wanted_scope) {
+-                      if ((found = str_array_find(entries, wanted_scope)))
++              const char *wanted;
++              found = TRUE;
++              array_foreach_elem(&req->db->set->scope, wanted) {
++                      if (!str_array_find(entries, wanted)) {
++                              found = FALSE;
+                               break;
++                      }
+               }
+       }
+       if (!found) {
diff --git a/patches/0002-doveadm-Avoid-leaking-doveadm_password-or-doveadm_ap.patch b/patches/0002-doveadm-Avoid-leaking-doveadm_password-or-doveadm_ap.patch
new file mode 100644 (file)
index 0000000..bb6a17b
--- /dev/null
@@ -0,0 +1,49 @@
+From 3c847240b1c0afc52ee570f45409f094c546a3a0 Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Tue, 28 Apr 2026 01:51:01 +0300
+Subject: [PATCH 2/2] doveadm: Avoid leaking doveadm_password or
+ doveadm_api_key lengths
+
+---
+ src/doveadm/client-connection-http.c | 4 ++--
+ src/doveadm/client-connection-tcp.c  | 2 +-
+ 2 files changed, 3 insertions(+), 3 deletions(-)
+
+diff --git a/src/doveadm/client-connection-http.c b/src/doveadm/client-connection-http.c
+index dfbc3528d7..f083ac6e14 100644
+--- a/src/doveadm/client-connection-http.c
++++ b/src/doveadm/client-connection-http.c
+@@ -973,7 +973,7 @@ doveadm_http_server_auth_basic(struct client_request_http *req,
+       b64_value = t_base64_encode_str(0, UINT_MAX, value);
+ 
+       if (creds->data != NULL &&
+-          str_equals_timing_almost_safe(str_c(b64_value), creds->data))
++          str_equals_hash_timing_safe(str_c(b64_value), creds->data))
+               return TRUE;
+ 
+       e_error(conn->conn.event,
+@@ -999,7 +999,7 @@ doveadm_http_server_auth_api_key(struct client_request_http *req,
+ 
+       b64_value = t_base64_encode_str(0, UINT_MAX, set->doveadm_api_key);
+       if (creds->data != NULL &&
+-          str_equals_timing_almost_safe(creds->data, str_c(b64_value)))
++          str_equals_hash_timing_safe(creds->data, str_c(b64_value)))
+               return TRUE;
+ 
+       e_error(conn->conn.event,
+diff --git a/src/doveadm/client-connection-tcp.c b/src/doveadm/client-connection-tcp.c
+index cb16f7214b..b374028c97 100644
+--- a/src/doveadm/client-connection-tcp.c
++++ b/src/doveadm/client-connection-tcp.c
+@@ -400,7 +400,7 @@ client_connection_tcp_authenticate(struct client_connection_tcp *conn)
+               return -1;
+       }
+       pass = t_strndup(data + 9, size - 9);
+-      if (!str_equals_timing_almost_safe(pass, set->doveadm_password)) {
++      if (!str_equals_hash_timing_safe(pass, set->doveadm_password)) {
+               e_error(conn->conn.event,
+                       "doveadm client authenticated with wrong password");
+               return -1;
+-- 
+2.47.3
+
diff --git a/patches/0002-doveadm-compress-connect-Don-t-hang-when-server-reje.patch b/patches/0002-doveadm-compress-connect-Don-t-hang-when-server-reje.patch
new file mode 100644 (file)
index 0000000..4840ba6
--- /dev/null
@@ -0,0 +1,120 @@
+From 4a731e5de4191134e27e14c62a42ae7201d26e26 Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Mon, 6 Jul 2026 07:17:42 +0000
+Subject: [PATCH 2/3] doveadm: compress-connect - Don't hang when server
+ rejects COMPRESS
+
+After sending a COMPRESS command the client stopped reading stdin and
+waited for the server's reply, but it only resumed on an "OK Begin
+compression" reply. If the server rejected COMPRESS (e.g. a NO reply for
+an unsupported mechanism), compress_waiting was never cleared: the client
+waited forever for an OK that never came while the server waited for the
+next command, deadlocking the session.
+
+Remember the COMPRESS command tag and treat any tagged reply to it that
+isn't the compression-start reply as a rejection, resuming the pipelined
+input.
+---
+ src/doveadm/doveadm-compress.c | 34 +++++++++++++++++++++++++++++++++-
+ 1 file changed, 33 insertions(+), 1 deletion(-)
+
+Index: trixie/src/doveadm/doveadm-compress.c
+===================================================================
+--- trixie.orig/src/doveadm/doveadm-compress.c
++++ trixie/src/doveadm/doveadm-compress.c
+@@ -20,6 +20,7 @@ enum server_input_line_type {
+       SERVER_INPUT_LINE_TYPE_DEFAULT,
+       SERVER_INPUT_LINE_TYPE_STARTTLS,
+       SERVER_INPUT_LINE_TYPE_COMPRESS,
++      SERVER_INPUT_LINE_TYPE_COMPRESS_FAILED,
+ };
+ 
+ struct client {
+@@ -32,6 +33,7 @@ struct client {
+       struct ssl_iostream *ssl_iostream;
+       const struct compression_handler *handler;
+       char *algorithm;
++      char *compress_tag;
+       bool tls;
+       bool compressed;
+       bool compress_waiting;
+@@ -111,17 +113,23 @@ cmd_dump_imap_compress(struct doveadm_cm
+ static bool
+ client_input_get_compress_algorithm(struct client *client, const char *line)
+ {
+-      const char *algorithm;
++      const char *algorithm, *tag = line;
++      size_t tag_len;
+ 
+       /* skip tag */
+       while (*line != ' ' && *line != '\0')
+               line++;
++      tag_len = line - tag;
+       if (!str_begins_icase(line, " COMPRESS ", &algorithm))
+               return FALSE;
+ 
+       if (compression_lookup_handler(t_str_lcase(algorithm),
+                                      &client->handler) <= 0)
+               i_fatal("Unsupported compression mechanism: %s", algorithm);
++      /* Remember the tag so we can tell whether the server accepted or
++         rejected this COMPRESS command. */
++      i_free(client->compress_tag);
++      client->compress_tag = i_strndup(tag, tag_len);
+       return TRUE;
+ }
+ 
+@@ -190,6 +198,13 @@ static bool server_input_is_compress_rep
+       return str_begins_with(line, " OK Begin compression");
+ }
+ 
++static bool server_input_is_reply_to_tag(const char *line, const char *tag)
++{
++      size_t tag_len = strlen(tag);
++
++      return strncmp(line, tag, tag_len) == 0 && line[tag_len] == ' ';
++}
++
+ static enum server_input_line_type
+ server_input_line_type(struct client *client, const char *line)
+ {
+@@ -197,6 +212,13 @@ server_input_line_type(struct client *cl
+               return SERVER_INPUT_LINE_TYPE_STARTTLS;
+       if (!client->compressed && server_input_is_compress_reply(line))
+               return SERVER_INPUT_LINE_TYPE_COMPRESS;
++      /* The server can reject COMPRESS (e.g. an unsupported mechanism). If we
++         are waiting for its reply, a tagged reply that isn't the "OK Begin
++         compression" above means the command failed - resume sending the
++         pipelined input instead of waiting forever. */
++      if (client->compress_waiting && client->compress_tag != NULL &&
++          server_input_is_reply_to_tag(line, client->compress_tag))
++              return SERVER_INPUT_LINE_TYPE_COMPRESS_FAILED;
+       return SERVER_INPUT_LINE_TYPE_DEFAULT;
+ }
+ 
+@@ -269,9 +291,18 @@ static void server_input(struct client *
+                       client->output = output;
+                       client->compressed = TRUE;
+                       client->compress_waiting = FALSE;
++                      i_free_and_null(client->compress_tag);
+                       i_stream_set_input_pending(client->stdin_input, TRUE);
+                       break;
+               }
++              case SERVER_INPUT_LINE_TYPE_COMPRESS_FAILED:
++                      /* Server rejected COMPRESS. Resume sending input. */
++                      e_info(client->event, "<Compression rejected>");
++                      client->handler = NULL;
++                      client->compress_waiting = FALSE;
++                      i_free_and_null(client->compress_tag);
++                      i_stream_set_input_pending(client->stdin_input, TRUE);
++                      break;
+               case SERVER_INPUT_LINE_TYPE_DEFAULT:
+                       break;
+               }
+@@ -329,6 +360,7 @@ static void cmd_compress_connect(struct
+       ssl_iostream_destroy(&client.ssl_iostream);
+       i_stream_unref(&client.stdin_input);
+       i_stream_unref(&client.input);
++      i_free(client.compress_tag);
+       o_stream_unref(&client.output);
+       event_unref(&client.event);
+       if (close(fd) < 0)
diff --git a/patches/0002-dsync-Fix-escaping-mail-or-attribute-value-that-begi.patch b/patches/0002-dsync-Fix-escaping-mail-or-attribute-value-that-begi.patch
new file mode 100644 (file)
index 0000000..05b5a01
--- /dev/null
@@ -0,0 +1,37 @@
+From a8b185e828d1a67ab66efd5c4a7d689e5eef0025 Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Tue, 24 Mar 2026 12:03:08 +0200
+Subject: [PATCH 2/2] dsync: Fix escaping mail or attribute value that begins
+ with a "." line
+
+Such a mail or attribute would have escaped the value parameter and the rest
+of the value would have been processed as dsync stream commands.
+---
+ src/doveadm/dsync/dsync-ibc-stream.c | 4 ++--
+ 1 file changed, 2 insertions(+), 2 deletions(-)
+
+diff --git a/src/doveadm/dsync/dsync-ibc-stream.c b/src/doveadm/dsync/dsync-ibc-stream.c
+index 0b3038201d..390c9a50c0 100644
+--- a/src/doveadm/dsync/dsync-ibc-stream.c
++++ b/src/doveadm/dsync/dsync-ibc-stream.c
+@@ -1533,7 +1533,7 @@ dsync_ibc_stream_send_mailbox_attribute(struct dsync_ibc *_ibc,
+       dsync_ibc_stream_send_string(ibc, str);
+ 
+       if (attr->value_stream != NULL) {
+-              ibc->value_output_last = '\0';
++              ibc->value_output_last = '\n';
+               ibc->value_output = attr->value_stream;
+               i_stream_ref(ibc->value_output);
+               (void)dsync_ibc_stream_send_value_stream(ibc);
+@@ -1913,7 +1913,7 @@ dsync_ibc_stream_send_mail(struct dsync_ibc *_ibc,
+       dsync_ibc_stream_send_string(ibc, str);
+ 
+       if (mail->input != NULL) {
+-              ibc->value_output_last = '\0';
++              ibc->value_output_last = '\n';
+               ibc->value_output = mail->input;
+               i_stream_ref(ibc->value_output);
+               (void)dsync_ibc_stream_send_value_stream(ibc);
+-- 
+2.47.3
+
diff --git a/patches/0002-imap-Recreate-multiplex-ostream-side-channel-after-u.patch b/patches/0002-imap-Recreate-multiplex-ostream-side-channel-after-u.patch
new file mode 100644 (file)
index 0000000..7590d91
--- /dev/null
@@ -0,0 +1,33 @@
+From d4fc8475f3b9cf8debbf9427d7bb8e9bbecb07ba Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Fri, 1 May 2026 17:40:17 +0000
+Subject: [PATCH 2/5] imap: Recreate multiplex ostream side channel after
+ unhibernation
+
+This will be needed by the following changes to send dict_reset commands via
+the side channel.
+---
+ src/imap/imap-state.c | 7 +++++++
+ 1 file changed, 7 insertions(+)
+
+diff --git a/src/imap/imap-state.c b/src/imap/imap-state.c
+index 3736296bea..fbd383d9de 100644
+--- a/src/imap/imap-state.c
++++ b/src/imap/imap-state.c
+@@ -723,6 +723,13 @@ import_state_compress(struct client *client, const unsigned char *data,
+               *error_r = t_strdup_printf("Unknown COMPRESS handler %s", name);
+               return 0;
+       }
++      /* Recreate the side channel that cmd-compress would have created in
++         the previous (now-exited) imap process. It is used to forward
++         dict_reset commands back to the imap-login proxy after every tagged
++         reply. */
++      if (client->multiplex_output != NULL &&
++          client->side_channel_output == NULL)
++              client_create_side_channel_output(client);
+       return p - data;
+ }
+ 
+-- 
+2.47.3
+
diff --git a/patches/0002-imap-Stream-BODYSTRUCTURE-to-client-without-ostream-.patch b/patches/0002-imap-Stream-BODYSTRUCTURE-to-client-without-ostream-.patch
new file mode 100644 (file)
index 0000000..84dd38a
--- /dev/null
@@ -0,0 +1,79 @@
+From 480e9dfb6140f51992fdf83a9e9418e1dcf742d2 Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Fri, 17 Apr 2026 16:55:05 +0200
+Subject: [PATCH 02/14] imap: Stream BODYSTRUCTURE to client without ostream
+ memory duplication
+
+For huge BODYSTRUCTURE responses (messages with many MIME parts),
+o_stream_send_str() caused the ostream to copy the entire string into
+its ring buffer when the stream was corked, doubling memory usage.
+
+Use the same o_stream_set_max_buffer_size(0) + o_stream_send_istream()
+pattern as fetch_stream_continue() so the ostream buffer stays at 0
+bytes and the I/O loop handles flow control via WAIT_OUTPUT.
+
+Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
+---
+ src/imap/imap-fetch.c | 37 +++++++++++++++++++++++++++++++++----
+ 1 file changed, 33 insertions(+), 4 deletions(-)
+
+diff --git a/src/imap/imap-fetch.c b/src/imap/imap-fetch.c
+index ca4b3216ce..b4a8a465d7 100644
+--- a/src/imap/imap-fetch.c
++++ b/src/imap/imap-fetch.c
+@@ -732,6 +732,34 @@ static bool fetch_body_init(struct imap_fetch_init_context *ctx)
+       return imap_fetch_body_section_init(ctx);
+ }
+ 
++static int fetch_bodystructure_stream_continue(struct imap_fetch_context *ctx)
++{
++      struct imap_fetch_state *state = &ctx->state;
++      enum ostream_send_istream_result res;
++
++      o_stream_set_max_buffer_size(ctx->client->output, 0);
++      res = o_stream_send_istream(ctx->client->output, state->cur_input);
++      o_stream_set_max_buffer_size(ctx->client->output, SIZE_MAX);
++
++      switch (res) {
++      case OSTREAM_SEND_ISTREAM_RESULT_FINISHED:
++              i_stream_unref(&state->cur_input);
++              state->cont_handler = NULL;
++              if (o_stream_send(ctx->client->output, ")", 1) < 0)
++                      return -1;
++              return 1;
++      case OSTREAM_SEND_ISTREAM_RESULT_WAIT_INPUT:
++              i_unreached();
++      case OSTREAM_SEND_ISTREAM_RESULT_WAIT_OUTPUT:
++              return 0;
++      case OSTREAM_SEND_ISTREAM_RESULT_ERROR_INPUT:
++              i_unreached();
++      case OSTREAM_SEND_ISTREAM_RESULT_ERROR_OUTPUT:
++              return -1;
++      }
++      i_unreached();
++}
++
+ static int fetch_bodystructure(struct imap_fetch_context *ctx,
+                              struct mail *mail, void *context ATTR_UNUSED)
+ {
+@@ -748,12 +776,13 @@ static int fetch_bodystructure(struct imap_fetch_context *ctx,
+                       return -1;
+       }
+ 
+-      if (o_stream_send(ctx->client->output, "BODYSTRUCTURE (", 15) < 0 ||
+-          o_stream_send_str(ctx->client->output, bodystructure) < 0 ||
+-          o_stream_send(ctx->client->output, ")", 1) < 0)
++      if (o_stream_send(ctx->client->output, "BODYSTRUCTURE (", 15) < 0)
+               return -1;
+ 
+-      return 1;
++      ctx->state.cur_input =
++              i_stream_create_from_data(bodystructure, strlen(bodystructure));
++      ctx->state.cont_handler = fetch_bodystructure_stream_continue;
++      return fetch_bodystructure_stream_continue(ctx);
+ }
+ 
+ static bool fetch_bodystructure_init(struct imap_fetch_init_context *ctx)
+-- 
+2.47.3
+
diff --git a/patches/0002-imap-hibernate-Use-imap-parser-API-for-parsing-DONE-.patch b/patches/0002-imap-hibernate-Use-imap-parser-API-for-parsing-DONE-.patch
new file mode 100644 (file)
index 0000000..945e040
--- /dev/null
@@ -0,0 +1,152 @@
+From fc1898a575fd4cf4c2a4b3e00da630825c0acf60 Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Wed, 22 Apr 2026 12:58:56 +0300
+Subject: [PATCH 2/2] imap-hibernate: Use imap-parser API for parsing DONE
+ command
+
+Replace the ad-hoc DONE/IDLE tokenizer with imap-parser.h: use
+imap_parser_read_tag() to read the tag and imap_parser_read_word() to
+read the DONE and IDLE keywords. This delegates bounds checks and
+character validation to the shared parser.
+---
+ src/imap-hibernate/imap-client.c | 103 ++++++++++++++++++-------------
+ 1 file changed, 59 insertions(+), 44 deletions(-)
+
+diff --git a/src/imap-hibernate/imap-client.c b/src/imap-hibernate/imap-client.c
+index 2110fe8e8e..564ef44f4f 100644
+--- a/src/imap-hibernate/imap-client.c
++++ b/src/imap-hibernate/imap-client.c
+@@ -21,6 +21,7 @@
+ #include "master-service-settings.h"
+ #include "imap-keepalive.h"
+ #include "imap-master-connection.h"
++#include "imap-parser.h"
+ #include "imap-client.h"
+ 
+ #include <unistd.h>
+@@ -334,64 +335,78 @@ static void imap_client_move_back(struct imap_client *client)
+ }
+ 
+ static enum imap_client_input_state
+-imap_client_input_parse(const unsigned char *data, size_t size, const char **tag_r)
++imap_client_input_parse_input(struct istream *input, struct imap_parser *parser,
++                            const char **tag_r)
+ {
+-      const unsigned char *tag_start, *tag_end;
+-
++      const unsigned char *rest;
++      size_t rest_size;
++      const char *word, *tag;
+       enum imap_client_input_state state = IMAP_CLIENT_INPUT_STATE_DONE_LF;
++      int ret;
+ 
+-      /* skip over DONE[\r]\n */
+-      if (i_memcasecmp(data, "DONE", I_MIN(size, 4)) != 0)
+-              return IMAP_CLIENT_INPUT_STATE_BAD;
+-      if (size <= 4)
++      /* read DONE */
++      word = imap_parser_read_word(parser);
++      if (word == NULL)
+               return IMAP_CLIENT_INPUT_STATE_UNKNOWN;
+-      data += 4; size -= 4;
++      if (strcasecmp(word, "DONE") != 0)
++              return IMAP_CLIENT_INPUT_STATE_BAD;
+ 
+-      if (data[0] == '\r') {
++      /* read [\r]\n after DONE */
++      rest = i_stream_get_data(input, &rest_size);
++      if (rest_size == 0)
++              return IMAP_CLIENT_INPUT_STATE_UNKNOWN;
++      if (rest[0] == '\r') {
+               state = IMAP_CLIENT_INPUT_STATE_DONE_CRLF;
+-              data++; size--;
++              i_stream_skip(input, 1);
++              rest = i_stream_get_data(input, &rest_size);
++              if (rest_size == 0)
++                      return IMAP_CLIENT_INPUT_STATE_UNKNOWN;
+       }
+-      if (size == 0)
+-              return IMAP_CLIENT_INPUT_STATE_UNKNOWN;
+-      if (data[0] != '\n')
++      if (rest[0] != '\n')
+               return IMAP_CLIENT_INPUT_STATE_BAD;
+-      data++; size--;
+-      if (size == 0)
++      i_stream_skip(input, 1);
++
++      /* optionally followed by "<tag> IDLE[\r]\n" - checking this assumes
++         that the DONE and IDLE are sent in the same IP packet, otherwise
++         we'll unnecessarily recreate the imap process and immediately resume
++         IDLE there. if this becomes an issue we could add a small delay to
++         the imap process creation and wait for the IDLE command during it. */
++      rest = i_stream_get_data(input, &rest_size);
++      if (rest_size == 0)
+               return state;
+ 
+-      tag_start = data;
+-
+-      /* skip over tag */
+-      while(size > 0 &&
+-            data[0] != ' ' &&
+-            data[0] != '\r' &&
+-            data[0] != '\t' &&
+-            data[0] != '\0') { data++; size--; }
+-
+-      tag_end = data;
+-
+-      if (size == 0)
++      ret = imap_parser_read_tag(parser, &tag);
++      if (ret <= 0)
+               return state;
+-      if (data[0] != ' ')
+-              return IMAP_CLIENT_INPUT_STATE_BAD;
+-      data++; size--;
+-
+-      /* skip over IDLE[\r]\n - checking this assumes that the DONE and IDLE
+-         are sent in the same IP packet, otherwise we'll unnecessarily
+-         recreate the imap process and immediately resume IDLE there. if this
+-         becomes an issue we could add a small delay to the imap process
+-         creation and wait for the IDLE command during it. */
+-      if (size <= 4 || i_memcasecmp(data, "IDLE", 4) != 0)
++
++      /* read IDLE */
++      word = imap_parser_read_word(parser);
++      if (word == NULL || strcasecmp(word, "IDLE") != 0)
+               return state;
+-      data += 4; size -= 4;
+ 
+-      if (data[0] == '\r') {
+-              data++; size--;
+-      }
+-      if (size == 1 && data[0] == '\n') {
+-              *tag_r = t_strdup_until(tag_start, tag_end);
+-              return IMAP_CLIENT_INPUT_STATE_DONEIDLE;
++      /* require [\r]\n and nothing else left */
++      rest = i_stream_get_data(input, &rest_size);
++      if (rest_size > 0 && rest[0] == '\r') {
++              i_stream_skip(input, 1);
++              rest = i_stream_get_data(input, &rest_size);
+       }
++      if (rest_size != 1 || rest[0] != '\n')
++              return state;
++
++      *tag_r = t_strdup(tag);
++      return IMAP_CLIENT_INPUT_STATE_DONEIDLE;
++}
++
++static enum imap_client_input_state
++imap_client_input_parse(const unsigned char *data, size_t size, const char **tag_r)
++{
++      struct istream *input = i_stream_create_from_data(data, size);
++      struct imap_parser *parser =
++              imap_parser_create(input, NULL, size, NULL);
++      enum imap_client_input_state state =
++              imap_client_input_parse_input(input, parser, tag_r);
++      imap_parser_unref(&parser);
++      i_stream_unref(&input);
+       return state;
+ }
+ 
+-- 
+2.47.3
+
diff --git a/patches/0002-lda-Default-mail_home-HOME-environment-if-not-using-.patch b/patches/0002-lda-Default-mail_home-HOME-environment-if-not-using-.patch
new file mode 100644 (file)
index 0000000..b5ac966
--- /dev/null
@@ -0,0 +1,34 @@
+From 42864e4182c7bfe2931fdf888f3cb4ba77953dee Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Mon, 26 May 2025 09:45:56 +0300
+Subject: [PATCH] lda: Default mail_home=$HOME environment if not using userdb
+ lookup
+
+The previous code to do this was removed by
+e57d5b9002f910c095ee5b55821395fcf1da016a
+---
+ src/lda/main.c | 5 +++++
+ 1 file changed, 5 insertions(+)
+
+Index: dovecot/src/lda/main.c
+===================================================================
+--- dovecot.orig/src/lda/main.c
++++ dovecot/src/lda/main.c
+@@ -478,12 +478,17 @@ int main(int argc, char *argv[])
+                                      "Couldn't lookup our username (uid=%s)",
+                                      dec2str(process_euid));
+               }
++              struct settings_root *set_root =
++                      master_service_get_settings_root(master_service);
++              settings_root_override(set_root, "mail_home", home,
++                                     SETTINGS_OVERRIDE_TYPE_DEFAULT);
+       } else {
+               i_fatal_status(EX_USAGE,
+                       "destination user parameter (-d user) not given");
+       }
+       struct master_service_settings_input set_input = {
+               .preserve_user = TRUE,
++              .preserve_home = TRUE,
+       };
+       struct master_service_settings_output set_output;
+       if (master_service_settings_read(master_service, &set_input,
diff --git a/patches/0002-lib-Replace-str_hash-strcase_hash-with-xxh64.patch b/patches/0002-lib-Replace-str_hash-strcase_hash-with-xxh64.patch
new file mode 100644 (file)
index 0000000..74e5a3a
--- /dev/null
@@ -0,0 +1,80 @@
+From bd11a6525eeb214541fa26450550f8b1bb4baec7 Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Mon, 13 Apr 2026 12:33:51 +0200
+Subject: [PATCH 02/12] lib: Replace str_hash/strcase_hash with xxh64
+
+Replaces the old ASU-derived shift-and-XOR hash with xxh64_to_32(),
+which provides better distribution and avalanche properties.
+strcase_hash feeds each i_toupper()'d byte through the streaming API.
+---
+ src/lib/hash.c | 41 +++++++++++------------------------------
+ 1 file changed, 11 insertions(+), 30 deletions(-)
+
+diff --git a/src/lib/hash.c b/src/lib/hash.c
+index aceef29b23..8f4cd7d9b4 100644
+--- a/src/lib/hash.c
++++ b/src/lib/hash.c
+@@ -5,6 +5,7 @@
+ #include "lib.h"
+ #include "hash.h"
+ #include "primes.h"
++#include "xxh64.h"
+ 
+ #include <ctype.h>
+ 
+@@ -519,42 +520,22 @@ void hash_table_copy(struct hash_table *dest, struct hash_table *src)
+       hash_table_thaw(dest);
+ }
+ 
+-/* a char* hash function from ASU -- from glib */
+-unsigned int ATTR_NO_SANITIZE_INTEGER
+-str_hash(const char *p)
++unsigned int str_hash(const char *p)
+ {
+-      const unsigned char *s = (const unsigned char *)p;
+-      unsigned int g, h = 0;
+-
+-      while (*s != '\0') {
+-              h = (h << 4) + *s;
+-              if ((g = h & 0xf0000000UL) != 0) {
+-                      h = h ^ (g >> 24);
+-                      h = h ^ g;
+-              }
+-              s++;
+-      }
+-
+-      return h;
++      return xxh64_to_32(xxh64_data(p, strlen(p), 0));
+ }
+ 
+-/* a char* hash function from ASU -- from glib */
+-unsigned int ATTR_NO_SANITIZE_INTEGER
+-strcase_hash(const char *p)
++unsigned int strcase_hash(const char *p)
+ {
+-      const unsigned char *s = (const unsigned char *)p;
+-      unsigned int g, h = 0;
++      struct xxh64_context ctx;
++      unsigned char c;
+ 
+-      while (*s != '\0') {
+-              h = (h << 4) + i_toupper(*s);
+-              if ((g = h & 0xf0000000UL) != 0) {
+-                      h = h ^ (g >> 24);
+-                      h = h ^ g;
+-              }
+-              s++;
++      xxh64_init(&ctx, 0);
++      while (*p != '\0') {
++              c = (unsigned char)i_toupper(*p++);
++              xxh64_loop(&ctx, &c, 1);
+       }
+-
+-      return h;
++      return xxh64_to_32(xxh64_result(&ctx));
+ }
+ 
+ unsigned int ATTR_NO_SANITIZE_INTEGER
+-- 
+2.47.3
+
diff --git a/patches/0002-lib-compression-Iterate-instead-of-recursing-on-zero.patch b/patches/0002-lib-compression-Iterate-instead-of-recursing-on-zero.patch
new file mode 100644 (file)
index 0000000..55c03c6
--- /dev/null
@@ -0,0 +1,187 @@
+From 88ae095f0530417ccc619af0a9fb68b0ef7cf0c0 Mon Sep 17 00:00:00 2001
+From: Aki Tuomi <aki.tuomi@open-xchange.com>
+Date: Wed, 10 Jun 2026 11:54:00 +0000
+Subject: [PATCH 2/4] lib-compression: Iterate instead of recursing on
+ zero-output decompress chunks
+
+istream-lz4, istream-zlib and istream-bzlib each retried a read that
+produced no output by tail-calling their own read function. A crafted
+compressed stream can contain an unbounded number of chunks/steps that
+each decompress to zero bytes - e.g. an lz4 stream of single-byte chunks
+that each decode to nothing - so an attacker controlling the compressed
+data can drive recursion depth proportional to the chunk count. Tail-call
+optimization is not guaranteed, so this can exhaust the stack and crash
+the process reading the stream.
+
+Replace the recursive calls with continue inside the for(;;) loop
+introduced in the previous commit, keeping stack usage O(1). Add a
+regression test that feeds istream-lz4 a stream of 100000 empty chunks
+and reads it in a single call.
+---
+ src/lib-compression/istream-bzlib.c    |  8 +++--
+ src/lib-compression/istream-lz4.c      | 16 ++++++---
+ src/lib-compression/istream-zlib.c     |  8 +++--
+ src/lib-compression/test-compression.c | 50 ++++++++++++++++++++++++++
+ 4 files changed, 74 insertions(+), 8 deletions(-)
+
+Index: trixie/src/lib-compression/istream-bzlib.c
+===================================================================
+--- trixie.orig/src/lib-compression/istream-bzlib.c
++++ trixie/src/lib-compression/istream-bzlib.c
+@@ -51,6 +51,10 @@ static ssize_t i_stream_bzlib_read(struc
+       size_t size, out_size;
+       int ret;
+ 
++      /* Loop instead of recursing when a decompress step yields no output:
++         a crafted stream can produce an unbounded number of zero-output
++         steps, and tail-call recursion (not guaranteed) would exhaust the
++         stack. */
+       for (;;) {
+               high_offset = stream->istream.v_offset + (stream->pos - stream->skip);
+               if (zstream->eof_offset == high_offset) {
+@@ -130,8 +134,8 @@ static ssize_t i_stream_bzlib_read(struc
+                       i_fatal("BZ2_bzDecompress() failed with %d", ret);
+               }
+               if (out_size == 0) {
+-                      /* read more input */
+-                      return i_stream_bzlib_read(stream);
++                      /* no output yet; read more input without recursing */
++                      continue;
+               }
+               return out_size;
+       }
+Index: trixie/src/lib-compression/istream-lz4.c
+===================================================================
+--- trixie.orig/src/lib-compression/istream-lz4.c
++++ trixie/src/lib-compression/istream-lz4.c
+@@ -149,6 +149,10 @@ static ssize_t i_stream_lz4_read(struct
+               zstream->header_read = TRUE;
+       }
+ 
++      /* Loop over chunks instead of recursing on zero-output chunks: a
++         crafted stream can contain an unbounded number of chunks that each
++         decompress to zero bytes, and tail-call recursion (which is not
++         guaranteed) would let that exhaust the stack. */
+       for (;;) {
+               if (zstream->chunk_left == 0) {
+                       while ((ret = i_stream_lz4_read_chunk_header(zstream)) == 0) {
+@@ -183,8 +187,8 @@ static ssize_t i_stream_lz4_read(struct
+               if (stream->pos - stream->skip >= i_stream_get_max_buffer_size(&stream->istream))
+                       return -2;
+               if (i_stream_get_data_size(zstream->istream.parent) > 0) {
+-                      /* Parent stream was only partially consumed. Set the stream's
+-                         IO as pending to avoid hangs. */
++                      /* Parent stream was only partially consumed. Set the
++                         stream's IO as pending to avoid hangs. */
+                       i_stream_set_input_pending(&zstream->istream.istream, TRUE);
+               }
+               /* allocate enough space for the old data and the new
+@@ -199,8 +203,12 @@ static ssize_t i_stream_lz4_read(struct
+                       lz4_read_error(zstream, "corrupted lz4 chunk");
+                       stream->istream.stream_errno = EINVAL;
+                       return -1;
+-              } else if (ret == 0)
+-                      return i_stream_lz4_read(stream);
++              } else if (ret == 0) {
++                      /* chunk decompressed to zero bytes; read the next chunk
++                         without recursing so the stack stays bounded. */
++                      buffer_set_used_size(zstream->chunk_buf, 0);
++                      continue;
++              }
+               i_assert(ret > 0);
+               stream->pos += ret;
+               i_assert(stream->pos <= stream->buffer_size);
+Index: trixie/src/lib-compression/istream-zlib.c
+===================================================================
+--- trixie.orig/src/lib-compression/istream-zlib.c
++++ trixie/src/lib-compression/istream-zlib.c
+@@ -168,6 +168,10 @@ static ssize_t i_stream_zlib_read(struct
+       size_t size, out_size;
+       int ret;
+ 
++      /* Loop instead of recursing when an inflate step yields no output:
++         a crafted stream can produce an unbounded number of zero-output
++         steps, and tail-call recursion (not guaranteed) would exhaust the
++         stack. */
+       for (;;) {
+               high_offset = stream->istream.v_offset + (stream->pos - stream->skip);
+               if (zstream->eof_offset == high_offset) {
+@@ -314,8 +318,8 @@ static ssize_t i_stream_zlib_read(struct
+                       i_fatal("inflate() failed with %d", ret);
+               }
+               if (out_size == 0) {
+-                      /* read more input */
+-                      return i_stream_zlib_read(stream);
++                      /* no output yet; read more input without recursing */
++                      continue;
+               }
+               return out_size;
+       }
+Index: trixie/src/lib-compression/test-compression.c
+===================================================================
+--- trixie.orig/src/lib-compression/test-compression.c
++++ trixie/src/lib-compression/test-compression.c
+@@ -1108,6 +1108,55 @@ static void test_lz4_chunk_size(void)
+       test_end();
+ }
+ 
++static void test_lz4_many_empty_chunks(void)
++{
++      const struct compression_handler *lz4;
++      struct istream *file_input, *input;
++
++      if (compression_lookup_handler("lz4", &lz4) <= 0)
++              return; /* not compiled in or unknown */
++
++      test_begin("lz4 many empty chunks");
++
++      /* A crafted lz4 stream can contain an unbounded number of chunks that
++         each decompress to zero bytes. i_stream_lz4_read() must iterate over
++         them rather than recurse once per chunk, or the stack is exhausted
++         (tail-call optimization is not guaranteed). */
++      buffer_t *buf = buffer_create_dynamic(default_pool, 1024*512);
++      struct iostream_lz4_header hdr;
++      memcpy(hdr.magic, IOSTREAM_LZ4_MAGIC, IOSTREAM_LZ4_MAGIC_LEN);
++      /* a valid (64k) max uncompressed chunk size, big-endian */
++      hdr.max_uncompressed_chunk_size[0] = 0x00;
++      hdr.max_uncompressed_chunk_size[1] = 0x01;
++      hdr.max_uncompressed_chunk_size[2] = 0x00;
++      hdr.max_uncompressed_chunk_size[3] = 0x00;
++      buffer_append(buf, &hdr, sizeof(hdr));
++
++      /* Each chunk: 4-byte big-endian compressed length (1), then a single
++         0x00 byte, which LZ4_decompress_safe() decodes to zero bytes. */
++      static const unsigned char empty_chunk[] = {
++              0x00, 0x00, 0x00, 0x01, 0x00
++      };
++      for (unsigned int i = 0; i < 100000; i++)
++              buffer_append(buf, empty_chunk, sizeof(empty_chunk));
++
++      file_input = test_istream_create_data(buf->data, buf->used);
++      file_input->blocking = TRUE;
++      input = lz4->create_istream(file_input);
++      i_stream_unref(&file_input);
++
++      /* All chunks are empty: clean EOF, no content, no error - and, with
++         the iterative read, no stack overflow. */
++      test_assert(i_stream_read(input) == -1);
++      test_assert(input->eof);
++      test_assert(input->stream_errno == 0);
++
++      i_stream_unref(&input);
++      buffer_free(&buf);
++
++      test_end();
++}
++
+ static void test_uncompress_file(const char *path)
+ {
+       const struct compression_handler *handler;
+@@ -1232,6 +1281,7 @@ int main(int argc, char *argv[])
+               test_gz_large_header,
+               test_lz4_small_header,
+               test_lz4_chunk_size,
++              test_lz4_many_empty_chunks,
+               test_compression_ext,
+               test_compression_deinit,
+               NULL
diff --git a/patches/0002-lib-mail-o_stream_dot_sendv-Fix-boundary-off-by-one.patch b/patches/0002-lib-mail-o_stream_dot_sendv-Fix-boundary-off-by-one.patch
new file mode 100644 (file)
index 0000000..a7ea806
--- /dev/null
@@ -0,0 +1,62 @@
+From a6d847f3ad7076a3406a84774a90ab58a67f7908 Mon Sep 17 00:00:00 2001
+From: Marco Bettini <marco.bettini@open-xchange.com>
+Date: Mon, 20 Apr 2026 09:50:54 +0000
+Subject: [PATCH 2/4] lib-mail: o_stream_dot_sendv() - Fix boundary off by one
+
+---
+ src/lib-mail/ostream-dot.c      |  2 +-
+ src/lib-mail/test-ostream-dot.c | 23 +++++++++++++++++++++++
+ 2 files changed, 24 insertions(+), 1 deletion(-)
+
+Index: trixie/src/lib-mail/ostream-dot.c
+===================================================================
+--- trixie.orig/src/lib-mail/ostream-dot.c
++++ trixie/src/lib-mail/ostream-dot.c
+@@ -105,7 +105,7 @@ o_stream_dot_sendv(struct ostream_privat
+ 
+               p = data;
+               pend = CONST_PTR_OFFSET(data, size);
+-              for (; p < pend && (size_t)(p-data)+2 < max_bytes; p++) {
++              for (; p < pend && ((size_t)(p - data) + 2) <= max_bytes; p++) {
+                       char add = 0;
+ 
+                       size = pend - p;
+Index: trixie/src/lib-mail/test-ostream-dot.c
+===================================================================
+--- trixie.orig/src/lib-mail/test-ostream-dot.c
++++ trixie/src/lib-mail/test-ostream-dot.c
+@@ -92,11 +92,34 @@ static void test_ostream_dot_parent_almo
+       test_end();
+ }
+ 
++static void test_ostream_dot_parent_exact_fit(void)
++{
++      buffer_t *output_data;
++      struct ostream *test_output, *output;
++      ssize_t ret;
++
++      test_begin("dot ostream parent exact fit");
++      output_data = t_buffer_create(1024);
++      test_output = test_ostream_create_nonblocking(output_data, 2);
++      test_ostream_set_max_output_size(test_output, 2);
++
++      output = o_stream_create_dot(test_output, FALSE);
++      ret = o_stream_send(output, ".", 1);
++      test_assert(ret == 1);
++      test_assert_ucmp(output_data->used, ==, 2);
++      test_assert_memcmp(output_data->data, output_data->used, "..", 2);
++
++      o_stream_unref(&output);
++      o_stream_unref(&test_output);
++      test_end();
++}
++
+ int main(void)
+ {
+       static void (*const test_functions[])(void) = {
+               test_ostream_dot,
+               test_ostream_dot_parent_almost_full,
++              test_ostream_dot_parent_exact_fit,
+               NULL
+       };
+       return test_run(test_functions);
diff --git a/patches/0002-lib-path-util-Add-t_openat_safe-for-jailed-openat-re.patch b/patches/0002-lib-path-util-Add-t_openat_safe-for-jailed-openat-re.patch
new file mode 100644 (file)
index 0000000..32ef957
--- /dev/null
@@ -0,0 +1,406 @@
+From 5cc55540eef2096680e6074c27ea0524a2ecc6d9 Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Mon, 4 May 2026 13:08:16 +0000
+Subject: [PATCH 2/5] lib: path-util - Add t_openat_safe() for jailed openat()
+ resolution
+
+Add a helper that opens a path relative to a directory fd while refusing
+any resolution that escapes that directory. Each component is opened with
+O_NOFOLLOW so symlinks are detected explicitly rather than transparently
+followed; symlinks are then resolved manually and only honoured when their
+(recursively resolved) target also stays beneath the base fd. Absolute
+symlink targets, leading '/', and '..' past the base are rejected with
+errno=ELOOP. Symlink chains are bounded by
+PATH_UTIL_OPENAT_SAFE_MAX_SYMLINKS hops.
+
+Anchoring resolution at a caller-held base_fd makes the lookup TOCTOU-safe
+even when intermediate path components are mutated on disk concurrently:
+the kernel resolves all openat() lookups relative to the original
+directory inode that base_fd refers to, so an attacker mutating path
+components on the filesystem cannot redirect resolution.
+
+This is the portable counterpart to Linux's openat2() with RESOLVE_BENEATH
+and is intended for callers that need to safely look up files under a
+user-writable directory (e.g. a Sieve script storage directory).
+---
+ src/lib/path-util.c      | 170 +++++++++++++++++++++++++++++++++++++++
+ src/lib/path-util.h      |  29 +++++++
+ src/lib/test-path-util.c | 124 ++++++++++++++++++++++++++++
+ 3 files changed, 323 insertions(+)
+
+Index: trixie/src/lib/path-util.c
+===================================================================
+--- trixie.orig/src/lib/path-util.c
++++ trixie/src/lib/path-util.c
+@@ -4,12 +4,17 @@
+ #include "str.h"
+ #include "path-util.h"
+ 
++#include <fcntl.h>
+ #include <unistd.h>
+ #include <sys/types.h>
+ #include <sys/stat.h>
+ 
+ #define PATH_UTIL_MAX_PATH      8*1024
+ #define PATH_UTIL_MAX_SYMLINKS  80
++/* Symlink-hop cap for t_openat_safe(). Lower than the realpath cap:
++   t_openat_safe() refuses absolute and escaping targets outright, so a
++   long chain inside the base dir is the only legitimate use. */
++#define PATH_UTIL_OPENAT_SAFE_MAX_SYMLINKS 16
+ 
+ static int t_getcwd_noalloc(char **dir_r, size_t *asize_r,
+                           const char **error_r) ATTR_NULL(2)
+@@ -358,6 +363,171 @@ int t_readlink(const char *path, const c
+       return 0;
+ }
+ 
++/* Iteratively resolve `path` beneath `cur_fd` using openat() with
++   O_NOFOLLOW per component. Symlinks are resolved manually: their targets
++   are validated (non-absolute, no '..' past the base) and the loop
++   restarts with the target spliced into the remaining path. dir_depth
++   tracks how many directory levels we have descended below the original
++   base; '..' is refused when the depth would go negative.
++ */
++static int
++path_openat_safe_walk(int cur_fd, const char *path, int flags,
++                    const char **error_r)
++{
++      bool cur_owned = FALSE;
++      unsigned int symlink_count = 0;
++      unsigned int dir_depth = 0;
++
++      /* Absolute paths cannot stay beneath the base directory. */
++      if (*path == '/') {
++              errno = ELOOP;
++              *error_r = "Path is absolute (escapes base directory)";
++              return -1;
++      }
++
++      for (;;) {
++              while (*path == '/')
++                      path++;
++
++              if (*path == '\0') {
++                      /* Empty path or trailing slash: open the current
++                         directory itself with the requested flags. */
++                      int fd = openat(cur_fd, ".",
++                                      flags | O_NOFOLLOW | O_CLOEXEC);
++                      if (cur_owned)
++                              i_close_fd(&cur_fd);
++                      if (fd < 0) {
++                              *error_r = t_strdup_printf(
++                                      "openat() failed: %m");
++                      }
++                      return fd;
++              }
++
++              const char *slash = strchr(path, '/');
++              size_t comp_len = (slash != NULL ?
++                                 (size_t)(slash - path) : strlen(path));
++              bool is_last = (slash == NULL);
++
++              if (comp_len == 1 && path[0] == '.') {
++                      path += comp_len;
++                      continue;
++              }
++
++              if (comp_len == 2 && path[0] == '.' && path[1] == '.') {
++                      if (dir_depth == 0) {
++                              if (cur_owned)
++                                      i_close_fd(&cur_fd);
++                              errno = ELOOP;
++                              *error_r = "Path escapes base directory via '..'";
++                              return -1;
++                      }
++                      int parent_fd = openat(cur_fd, "..",
++                                             O_RDONLY | O_DIRECTORY |
++                                             O_NOFOLLOW | O_CLOEXEC);
++                      if (cur_owned)
++                              i_close_fd(&cur_fd);
++                      if (parent_fd < 0) {
++                              *error_r = t_strdup_printf(
++                                      "openat(..) failed: %m");
++                              return -1;
++                      }
++                      cur_fd = parent_fd;
++                      cur_owned = TRUE;
++                      dir_depth--;
++                      path += comp_len;
++                      continue;
++              }
++
++              const char *comp = t_strdup_until(path, path + comp_len);
++              int next_fd;
++              if (is_last) {
++                      next_fd = openat(cur_fd, comp,
++                                       flags | O_NOFOLLOW | O_CLOEXEC);
++              } else {
++                      next_fd = openat(cur_fd, comp,
++                                       O_RDONLY | O_DIRECTORY |
++                                       O_NOFOLLOW | O_CLOEXEC);
++              }
++
++              if (next_fd < 0 && (errno == ELOOP || errno == EMLINK)) {
++                      /* Component is a symlink. Resolve it ourselves and
++                         restart processing with its target spliced into
++                         `path`; that way the target is itself walked
++                         component-by-component and the boundary check
++                         continues to apply. (O_NOFOLLOW on a symlink reports
++                         ELOOP on Linux/macOS but EMLINK on the BSDs, so both
++                         must be treated as "this is a symlink".) */
++                      if (++symlink_count >
++                          PATH_UTIL_OPENAT_SAFE_MAX_SYMLINKS) {
++                              if (cur_owned)
++                                      i_close_fd(&cur_fd);
++                              errno = ELOOP;
++                              *error_r = "Too many symlink dereferences";
++                              return -1;
++                      }
++
++                      char buf[PATH_UTIL_MAX_PATH];
++                      ssize_t llen = readlinkat(cur_fd, comp,
++                                                buf, sizeof(buf) - 1);
++                      if (llen < 0) {
++                              if (cur_owned)
++                                      i_close_fd(&cur_fd);
++                              *error_r = t_strdup_printf(
++                                      "readlinkat(%s) failed: %m", comp);
++                              return -1;
++                      }
++                      buf[llen] = '\0';
++
++                      if (buf[0] == '/') {
++                              if (cur_owned)
++                                      i_close_fd(&cur_fd);
++                              errno = ELOOP;
++                              *error_r = t_strdup_printf(
++                                      "Symlink '%s' has absolute target "
++                                      "'%s' (escapes base directory)",
++                                      comp, buf);
++                              return -1;
++                      }
++
++                      path = is_last ? t_strdup(buf) :
++                                       t_strconcat(buf, "/",
++                                                   slash + 1, NULL);
++                      continue;
++              }
++
++              if (next_fd < 0) {
++                      if (cur_owned)
++                              i_close_fd(&cur_fd);
++                      *error_r = t_strdup_printf(
++                              "openat(%s) failed: %m", comp);
++                      return -1;
++              }
++
++              if (is_last) {
++                      if (cur_owned)
++                              i_close_fd(&cur_fd);
++                      return next_fd;
++              }
++
++              if (cur_owned)
++                      i_close_fd(&cur_fd);
++              cur_fd = next_fd;
++              cur_owned = TRUE;
++              dir_depth++;
++              path = slash + 1;
++      }
++}
++
++int t_openat_safe(int base_fd, const char *path, int flags,
++                const char **error_r)
++{
++      i_assert(base_fd >= 0);
++      i_assert(path != NULL);
++      i_assert(error_r != NULL);
++
++      return path_openat_safe_walk(base_fd, path, flags, error_r);
++}
++
+ bool t_binary_abspath(const char **binpath, const char **error_r)
+ {
+       const char *path_env, *const *paths;
+Index: trixie/src/lib/path-util.h
+===================================================================
+--- trixie.orig/src/lib/path-util.h
++++ trixie/src/lib/path-util.h
+@@ -57,6 +57,35 @@ int t_get_working_dir(const char **dir_r
+  * -1 on failure. error_r is set on failure and cannot be NULL. */
+ int t_readlink(const char *path, const char **dest_r, const char **error_r);
+ 
++/* Open `path` relative to `base_fd`, refusing any resolution that escapes
++   the directory referenced by `base_fd`. Each path component is opened with
++   O_NOFOLLOW so symlinks are detected explicitly rather than transparently
++   followed; symlinks are followed only when their (recursively resolved)
++   target also stays beneath `base_fd`. Absolute symlink targets, leading
++   '/', and '..' past the base are rejected.
++
++   `base_fd` must reference a directory (e.g. obtained via
++   open(dir, O_DIRECTORY|O_RDONLY|O_CLOEXEC)) and remains owned by the
++   caller; this function does not close it.
++
++   `flags` is OR-ed into the openat() call for the final component (e.g.
++   O_RDONLY). O_NOFOLLOW and O_CLOEXEC are added implicitly.
++
++   Anchoring resolution at `base_fd` makes the lookup TOCTOU-safe even when
++   intermediate path components are mutated on disk concurrently: the kernel
++   resolves all component lookups relative to the original directory inode
++   `base_fd` references.
++
++   Returns the new fd (>= 0) on success. On failure returns -1 with errno
++   set and *error_r set to a descriptive message:
++    errno=ELOOP   target escapes base, or symlink chain too deep
++    errno=ENOENT  file not found
++    errno=EACCES  permission denied
++    other errno   as set by openat()/readlinkat()
++ */
++int t_openat_safe(int base_fd, const char *path, int flags,
++                const char **error_r);
++
+ /* Update binpath to be absolute:
+  * a) begins with '/' -> no change
+  * b) contains '/' -> assume relative to working directory
+Index: trixie/src/lib/test-path-util.c
+===================================================================
+--- trixie.orig/src/lib/test-path-util.c
++++ trixie/src/lib/test-path-util.c
+@@ -233,6 +233,129 @@ static void test_link_alloc2(void)
+       tmpdir = o_tmpdir;
+ }
+ 
++static void test_openat_safe(void)
++{
++      const char *error;
++      int base_fd, fd;
++
++      base_fd = open(tmpdir, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
++      if (base_fd < 0)
++              i_fatal("open(%s) failed: %m", tmpdir);
++
++      /* Plain regular file inside base. */
++      const char *plain = t_strconcat(tmpdir, "/plain", NULL);
++      int wfd = creat(plain, 0600);
++      if (wfd < 0)
++              i_fatal("creat(%s) failed: %m", plain);
++      i_close_fd(&wfd);
++
++      fd = t_openat_safe(base_fd, "plain", O_RDONLY, &error);
++      test_assert(fd >= 0);
++      i_close_fd(&fd);
++
++      /* "./plain" and redundant slashes. */
++      fd = t_openat_safe(base_fd, "./plain", O_RDONLY, &error);
++      test_assert(fd >= 0);
++      i_close_fd(&fd);
++      fd = t_openat_safe(base_fd, ".//plain", O_RDONLY, &error);
++      test_assert(fd >= 0);
++      i_close_fd(&fd);
++
++      /* Symlink whose target is inside the base: allowed. */
++      const char *in_link = t_strconcat(tmpdir, "/in-link", NULL);
++      if (symlink("plain", in_link) < 0)
++              i_fatal("symlink(plain, %s) failed: %m", in_link);
++      fd = t_openat_safe(base_fd, "in-link", O_RDONLY, &error);
++      test_assert(fd >= 0);
++      i_close_fd(&fd);
++
++      /* Absolute symlink target: refused. */
++      const char *abs_link = t_strconcat(tmpdir, "/abs-link", NULL);
++      if (symlink("/etc/hostname", abs_link) < 0)
++              i_fatal("symlink failed: %m");
++      errno = 0;
++      fd = t_openat_safe(base_fd, "abs-link", O_RDONLY, &error);
++      test_assert(fd == -1);
++      test_assert(errno == ELOOP);
++      test_assert(error != NULL);
++
++      /* Relative symlink with '..' that escapes the base: refused. */
++      const char *escape_link = t_strconcat(tmpdir, "/escape-link", NULL);
++      if (symlink("../../etc/hostname", escape_link) < 0)
++              i_fatal("symlink failed: %m");
++      errno = 0;
++      fd = t_openat_safe(base_fd, "escape-link", O_RDONLY, &error);
++      test_assert(fd == -1);
++      test_assert(errno == ELOOP);
++      test_assert(error != NULL);
++
++      /* Direct '..' escape attempt: refused. */
++      errno = 0;
++      fd = t_openat_safe(base_fd, "../plain", O_RDONLY, &error);
++      test_assert(fd == -1);
++      test_assert(errno == ELOOP);
++
++      /* Absolute literal path: refused. */
++      errno = 0;
++      fd = t_openat_safe(base_fd, "/etc/hostname", O_RDONLY, &error);
++      test_assert(fd == -1);
++      test_assert(errno == ELOOP);
++
++      /* link2's target is an absolute path (tmpdir/link3): refused as an
++         absolute target before the loop is ever followed. */
++      errno = 0;
++      fd = t_openat_safe(base_fd, "link2", O_RDONLY, &error);
++      test_assert(fd == -1);
++      test_assert(errno == ELOOP);
++
++      /* Relative symlink loop staying inside the base: nothing rejects it
++         except the hop counter, so this exercises
++         PATH_UTIL_OPENAT_SAFE_MAX_SYMLINKS. */
++      const char *loopa = t_strconcat(tmpdir, "/loopa", NULL);
++      const char *loopb = t_strconcat(tmpdir, "/loopb", NULL);
++      if (symlink("loopb", loopa) < 0)
++              i_fatal("symlink(loopb, %s) failed: %m", loopa);
++      if (symlink("loopa", loopb) < 0)
++              i_fatal("symlink(loopa, %s) failed: %m", loopb);
++      errno = 0;
++      fd = t_openat_safe(base_fd, "loopa", O_RDONLY, &error);
++      test_assert(fd == -1);
++      test_assert(errno == ELOOP);
++      test_assert(error != NULL);
++
++      /* '..' that stays within the base: allowed. Create sub/under and
++         reach back to plain via sub/../plain. */
++      const char *subdir = t_strconcat(tmpdir, "/sub", NULL);
++      if (mkdir(subdir, 0700) < 0 && errno != EEXIST)
++              i_fatal("mkdir(%s) failed: %m", subdir);
++      fd = t_openat_safe(base_fd, "sub/../plain", O_RDONLY, &error);
++      test_assert(fd >= 0);
++      i_close_fd(&fd);
++
++      /* Breaking out of the base via subdirectory. */
++      errno = 0;
++      fd = t_openat_safe(base_fd, "sub/../../plain", O_RDONLY, &error);
++      test_assert(fd == -1);
++      test_assert(errno == ELOOP);
++
++      /* Nonexistent file inside base: ENOENT, not ELOOP. */
++      errno = 0;
++      fd = t_openat_safe(base_fd, "no-such-file", O_RDONLY, &error);
++      test_assert(fd == -1);
++      test_assert(errno == ENOENT);
++
++      i_close_fd(&base_fd);
++
++      i_unlink(plain);
++      i_unlink(in_link);
++      i_unlink(abs_link);
++      i_unlink(escape_link);
++      i_unlink(loopa);
++      i_unlink(loopb);
++      if (rmdir(subdir) < 0)
++              i_error("rmdir(%s) failed: %m", subdir);
++}
++
+ static void test_cleanup(void)
+ {
+       const char *error;
+@@ -272,6 +395,7 @@ void test_path_util(void)
+       test_abspath_vs_normpath();
+       test_link_alloc();
+       test_link_alloc2();
++      test_openat_safe();
+       test_cleanup();
+       alarm(0);
+       test_end();
diff --git a/patches/0002-lib-sieve-Drop-binary-header-resource-usage-tracking.patch b/patches/0002-lib-sieve-Drop-binary-header-resource-usage-tracking.patch
new file mode 100644 (file)
index 0000000..35fe8b2
--- /dev/null
@@ -0,0 +1,304 @@
+From 64e1f7af273809f484fb983d4a6edb1af1a85c99 Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Tue, 5 May 2026 16:19:20 +0000
+Subject: [PATCH 2/7] lib-sieve: Drop binary-header resource usage tracking
+
+With the per-user sieve-rusage file now authoritative for personal
+storages, the .svbin header no longer needs to carry CPU usage. Admin
+storages (sieve_before/after, default location) cannot be re-uploaded
+by the user, so cross-run tracking is unnecessary there; the runtime
+sieve_max_cpu_time check still enforces the per-execution limit.
+
+Move the in-memory cumulative carry-over from sbin->header.resource_usage
+to a dedicated sbin->persisted_rusage substruct. The on-disk header
+struct now solely represents the wire format and is zeroed on every
+save; the in-memory carry-over no longer overloads it. Drop the now-dead
+"resource usage" section from sieve_binary_dumper_run() (the on-disk
+fields are always zero, so the section never produced output).
+---
+ src/lib-sieve/sieve-binary-dumper.c  |  12 ---
+ src/lib-sieve/sieve-binary-file.c    | 141 +++++----------------------
+ src/lib-sieve/sieve-binary-private.h |   6 ++
+ src/lib-sieve/sieve-binary.c         |  15 ++-
+ 4 files changed, 37 insertions(+), 137 deletions(-)
+
+Index: trixie/pigeonhole/src/lib-sieve/sieve-binary-dumper.c
+===================================================================
+--- trixie.orig/pigeonhole/src/lib-sieve/sieve-binary-dumper.c
++++ trixie/pigeonhole/src/lib-sieve/sieve-binary-dumper.c
+@@ -6,7 +6,6 @@
+ #include "ostream.h"
+ #include "array.h"
+ #include "buffer.h"
+-#include "time-util.h"
+ 
+ #include "sieve-common.h"
+ #include "sieve-extensions.h"
+@@ -117,17 +116,6 @@ bool sieve_binary_dumper_run(struct siev
+               "flags = 0x%08"PRIx32"\n",
+               header->version_major, header->version_minor,
+               header->flags);
+-      if (header->resource_usage.update_time != 0) {
+-              time_t update_time =
+-                      (time_t)header->resource_usage.update_time;
+-              sieve_binary_dumpf(denv,
+-                      "resource usage:\n"
+-                      "  update time = %s\n"
+-                      "  cpu time = %"PRIu32" ms\n",
+-                      t_strflocaltime("%Y-%m-%d %H:%M:%S",
+-                                      update_time),
+-                      header->resource_usage.cpu_time_msecs);
+-      }
+ 
+       /* Dump list of binary blocks */
+ 
+Index: trixie/pigeonhole/src/lib-sieve/sieve-binary-file.c
+===================================================================
+--- trixie.orig/pigeonhole/src/lib-sieve/sieve-binary-file.c
++++ trixie/pigeonhole/src/lib-sieve/sieve-binary-file.c
+@@ -147,47 +147,15 @@ sieve_binary_file_read_header(struct sie
+       return 0;
+ }
+ 
+-static int
+-sieve_binary_file_write_header(struct sieve_binary *sbin, int fd,
+-                             struct sieve_binary_header *header,
+-                             enum sieve_error *error_code_r)
+-{
+-      ssize_t wret;
+-
+-      wret = pwrite(fd, header, sizeof(*header), 0);
+-      if (wret < 0) {
+-              e_error(sbin->event, "update: "
+-                      "failed to write to binary: %m");
+-              *error_code_r = SIEVE_ERROR_TEMP_FAILURE;
+-              return -1;
+-      } else if (wret != sizeof(*header)) {
+-              e_error(sbin->event, "update: "
+-                      "header written partially %zd/%zu",
+-                      wret, sizeof(*header));
+-              *error_code_r = SIEVE_ERROR_TEMP_FAILURE;
+-              return -1;
+-      }
+-      return 0;
+-}
+-
+-static void sieve_binary_file_update_header(struct sieve_binary *sbin)
++/* The on-disk binary header carries an unused resource_usage struct for
++   backward compatibility. Tracking lives in the per-user sieve-rusage file. */
++static void sieve_binary_file_zero_header_rusage(struct sieve_binary *sbin)
+ {
+       struct sieve_binary_header *header = &sbin->header;
+-      struct sieve_resource_usage rusage;
+-
+-      sieve_binary_get_resource_usage(sbin, &rusage);
+ 
+       i_zero(&header->resource_usage);
+-      if (HAS_ALL_BITS(header->flags, SIEVE_BINARY_FLAG_RESOURCE_LIMIT) ||
+-          sieve_resource_usage_is_high(sbin->svinst, &rusage)) {
+-              header->resource_usage.update_time = ioloop_time;
+-              header->resource_usage.cpu_time_msecs = rusage.cpu_time_msecs;
+-      }
+-
+       sieve_resource_usage_init(&sbin->rusage);
+       sbin->rusage_updated = FALSE;
+-
+-      (void)sieve_binary_check_resource_usage(sbin);
+ }
+ 
+ /*
+@@ -347,7 +315,7 @@ sieve_binary_save_to_stream(struct sieve
+       header->hdr_size = sizeof(*header);
+ 
+       header->flags &= ENUM_NEGATE(SIEVE_BINARY_FLAG_RESOURCE_LIMIT);
+-      sieve_binary_file_update_header(sbin);
++      sieve_binary_file_zero_header_rusage(sbin);
+ 
+       if (!_save_aligned(sbin, stream, header, sizeof(*header), NULL)) {
+               e_error(sbin->event, "save: failed to save header");
+@@ -864,6 +832,11 @@ _sieve_binary_open(struct sieve_binary *
+               return FALSE;
+       offset = sbin->header.hdr_size;
+ 
++      /* Resource usage tracking is no longer kept in the binary file;
++         discard any data from older versions. */
++      i_zero(&sbin->header.resource_usage);
++      sbin->header.flags &= ENUM_NEGATE(SIEVE_BINARY_FLAG_RESOURCE_LIMIT);
++
+       /* Load block index */
+ 
+       for (i = 0; i < sbin->header.blocks && result; i++) {
+@@ -979,47 +952,27 @@ int sieve_binary_check_executable(struct
+  * Resource usage
+  */
+ 
+-static int
+-sieve_binary_file_do_update_resource_usage(
+-      struct sieve_binary *sbin, int fd, enum sieve_error *error_code_r)
+-{
+-      struct sieve_binary_header *header = &sbin->header;
+-      struct file_lock *lock;
+-      const char *error;
+-      int ret;
+-
+-      struct file_lock_settings lock_set = {
+-              .lock_method = FILE_LOCK_METHOD_FCNTL,
+-      };
+-      ret = file_wait_lock(fd, sbin->path, F_WRLCK, &lock_set,
+-                           SIEVE_BINARY_FILE_LOCK_TIMEOUT, &lock, &error);
+-      if (ret <= 0) {
+-              e_error(sbin->event, "%s", error);
+-              *error_code_r = SIEVE_ERROR_TEMP_FAILURE;
+-              return -1;
+-      }
+-
+-      ret = sieve_binary_file_read_header(sbin, fd, header, error_code_r);
+-      if (ret == 0) {
+-              sieve_binary_file_update_header(sbin);
+-              ret = sieve_binary_file_write_header(sbin, fd, header,
+-                                                   error_code_r);
+-      }
+-
+-      file_lock_free(&lock);
+-
+-      return ret;
+-}
+-
+-static int
+-sieve_binary_storage_update_resource_usage(struct sieve_binary *sbin,
+-                                         struct sieve_storage *storage,
+-                                         enum sieve_error *error_code_r)
++int sieve_binary_file_update_resource_usage(struct sieve_binary *sbin,
++                                          enum sieve_error *error_code_r)
+ {
++      struct sieve_storage *storage;
+       struct sieve_resource_usage delta;
+       struct sieve_resource_usage total;
+       uint32_t flags;
+ 
++      sieve_error_args_init(&error_code_r, NULL);
++
++      sieve_binary_file_close(&sbin->file);
++
++      if (sbin->script == NULL || sbin->script->storage == NULL ||
++          sbin->script->storage->rusage_path == NULL) {
++              /* No per-user file: tracking discarded. */
++              sieve_resource_usage_init(&sbin->rusage);
++              sbin->rusage_updated = FALSE;
++              return 0;
++      }
++      storage = sbin->script->storage;
++
+       delta = sbin->rusage;
+       sieve_binary_get_resource_usage(sbin, &total);
+       flags = sbin->header.flags & SIEVE_BINARY_FLAG_RESOURCE_LIMIT;
+@@ -1041,47 +994,3 @@ sieve_binary_storage_update_resource_usa
+       sbin->rusage_updated = FALSE;
+       return 0;
+ }
+-
+-int sieve_binary_file_update_resource_usage(struct sieve_binary *sbin,
+-                                          enum sieve_error *error_code_r)
+-{
+-      struct sieve_storage *storage = NULL;
+-      int fd, ret = 0;
+-
+-      sieve_error_args_init(&error_code_r, NULL);
+-
+-      sieve_binary_file_close(&sbin->file);
+-
+-      if (sbin->script != NULL && sbin->script->storage != NULL &&
+-          sbin->script->storage->rusage_path != NULL)
+-              storage = sbin->script->storage;
+-
+-      if (storage != NULL) {
+-              return sieve_binary_storage_update_resource_usage(
+-                      sbin, storage, error_code_r);
+-      }
+-
+-      if (sbin->path == NULL)
+-              return 0;
+-      if (sbin->header.version_major != SIEVE_BINARY_VERSION_MAJOR) {
+-              return sieve_binary_save(sbin, sbin->path, TRUE, 0600,
+-                                       error_code_r);
+-      }
+-
+-      fd = sieve_binary_fd_open(sbin, sbin->path, O_RDWR, error_code_r);
+-      if (fd < 0) {
+-              i_assert(*error_code_r != SIEVE_ERROR_NONE);
+-              return -1;
+-      }
+-
+-      ret = sieve_binary_file_do_update_resource_usage(sbin, fd,
+-                                                       error_code_r);
+-      i_assert(ret == 0 || *error_code_r != SIEVE_ERROR_NONE);
+-
+-      if (close(fd) < 0) {
+-              e_error(sbin->event, "update: "
+-                      "failed to close: close() failed: %m");
+-      }
+-
+-      return ret;
+-}
+Index: trixie/pigeonhole/src/lib-sieve/sieve-binary-private.h
+===================================================================
+--- trixie.orig/pigeonhole/src/lib-sieve/sieve-binary-private.h
++++ trixie/pigeonhole/src/lib-sieve/sieve-binary-private.h
+@@ -98,6 +98,12 @@ struct sieve_binary {
+       struct sieve_binary_header header;
+       struct sieve_resource_usage rusage;
+ 
++      /* Cumulative CPU usage loaded from per-user sieve-rusage file */
++      struct {
++              time_t update_time;
++              uint32_t cpu_time_msecs;
++      } persisted_rusage;
++
+       /* When the binary is loaded into memory or when it is being constructed
+          by the generator, extensions can be associated to the binary. The
+          extensions array is a sequential list of all linked extensions. The
+Index: trixie/pigeonhole/src/lib-sieve/sieve-binary.c
+===================================================================
+--- trixie.orig/pigeonhole/src/lib-sieve/sieve-binary.c
++++ trixie/pigeonhole/src/lib-sieve/sieve-binary.c
+@@ -218,8 +218,8 @@ void sieve_binary_apply_persisted_rusage
+       if (sieve_rusage_storage_load(storage, &rusage, &flags) <= 0)
+               return;
+ 
+-      header->resource_usage.cpu_time_msecs = rusage.cpu_time_msecs;
+-      header->resource_usage.update_time = ioloop_time;
++      sbin->persisted_rusage.cpu_time_msecs = rusage.cpu_time_msecs;
++      sbin->persisted_rusage.update_time = ioloop_time;
+       header->flags |= (flags & SIEVE_BINARY_FLAG_RESOURCE_LIMIT);
+       sieve_resource_usage_init(&sbin->rusage);
+       sbin->rusage_updated = FALSE;
+@@ -228,15 +228,14 @@ void sieve_binary_apply_persisted_rusage
+ void sieve_binary_get_resource_usage(struct sieve_binary *sbin,
+                                    struct sieve_resource_usage *rusage_r)
+ {
+-      struct sieve_binary_header *header = &sbin->header;
+-      time_t update_time = header->resource_usage.update_time;
++      time_t update_time = sbin->persisted_rusage.update_time;
+       unsigned int timeout = sbin->svinst->set->resource_usage_timeout;
+ 
+       if (update_time != 0 && (ioloop_time - update_time) > (time_t)timeout)
+-              i_zero(&header->resource_usage);
++              i_zero(&sbin->persisted_rusage);
+ 
+       sieve_resource_usage_init(rusage_r);
+-      rusage_r->cpu_time_msecs = header->resource_usage.cpu_time_msecs;
++      rusage_r->cpu_time_msecs = sbin->persisted_rusage.cpu_time_msecs;
+       sieve_resource_usage_add(rusage_r, &sbin->rusage);
+ }
+ 
+@@ -278,9 +277,7 @@ bool sieve_binary_record_resource_usage(
+ void sieve_binary_set_resource_usage(struct sieve_binary *sbin,
+                                    const struct sieve_resource_usage *rusage)
+ {
+-      struct sieve_binary_header *header = &sbin->header;
+-
+-      i_zero(&header->resource_usage);
++      i_zero(&sbin->persisted_rusage);
+       sbin->rusage = *rusage;
+       sbin->rusage_updated = TRUE;
+ 
diff --git a/patches/0002-lib-sieve-actions-Create-fileinto-mailbox-without-th.patch b/patches/0002-lib-sieve-actions-Create-fileinto-mailbox-without-th.patch
new file mode 100644 (file)
index 0000000..9b4e91f
--- /dev/null
@@ -0,0 +1,146 @@
+From eec184745fabe1b2da0d5cc343f2a46710b70fc7 Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Thu, 11 Jun 2026 19:33:23 +0000
+Subject: [PATCH 2/2] lib-sieve: actions - Create fileinto mailbox without the
+ autocreate flag
+
+Stop setting MAILBOX_FLAG_AUTO_CREATE/SUBSCRIBE in act_store. Instead, on
+mailbox_open() returning MAIL_ERROR_NOTFOUND, create the mailbox explicitly
+with sieve_act_store_create_mailbox() when autocreation is enabled (the
+:create side effect already creates it explicitly).
+
+This makes the creation go through the normal ACL CREATE gate, so a fileinto
+(with lda_mailbox_autocreate=yes) or fileinto :create can no longer create a
+mailbox in a shared or public namespace that the user lacks the CREATE right
+for.
+
+A permission failure to create the mailbox is classified by
+sieve_act_store_create_error_status(): for a user's personal script it is a
+permanent failure (implicit keep), while for a global (administrator-defined)
+script it is deferred (temporary failure), so the administrator notices the
+script is behaving in a way they did not intend.
+---
+ src/lib-sieve/sieve-actions.c | 56 +++++++++++++++++++++++++++--------
+ src/lib-sieve/sieve-actions.h | 11 ++++++-
+ 2 files changed, 53 insertions(+), 14 deletions(-)
+
+Index: trixie/pigeonhole/src/lib-sieve/sieve-actions.c
+===================================================================
+--- trixie.orig/pigeonhole/src/lib-sieve/sieve-actions.c
++++ trixie/pigeonhole/src/lib-sieve/sieve-actions.c
+@@ -372,6 +372,31 @@ void sieve_act_store_get_storage_error(c
+                                               &trans->error_code));
+ }
+ 
++int sieve_act_store_create_error_status(
++      const struct sieve_action_exec_env *aenv, enum mail_error error_code)
++{
++      const struct sieve_execute_env *eenv = aenv->exec_env;
++
++      switch (error_code) {
++      case MAIL_ERROR_NONE:
++              return SIEVE_EXEC_OK;
++      case MAIL_ERROR_TEMP:
++              return SIEVE_EXEC_TEMP_FAILURE;
++      case MAIL_ERROR_PERM:
++              /* The mailbox cannot be created because of insufficient
++                 permissions. This is a permanent condition. For a user's
++                 personal script, fall through to implicit keep rather than
++                 deferring the message. For a global (administrator-defined)
++                 script, defer instead, so that the administrator notices the
++                 script is behaving in a way they did not intend. */
++              if ((eenv->flags & SIEVE_EXECUTE_FLAG_NOGLOBAL) != 0)
++                      return SIEVE_EXEC_FAILURE;
++              return SIEVE_EXEC_TEMP_FAILURE;
++      default:
++              return SIEVE_EXEC_FAILURE;
++      }
++}
++
+ int sieve_act_store_create_mailbox(const struct sieve_action_exec_env *aenv,
+                                  struct act_store_transaction *trans)
+ {
+@@ -384,13 +409,13 @@ int sieve_act_store_create_mailbox(const
+       /* Create mailbox */
+       if (mailbox_create(box, NULL, FALSE) < 0) {
+               sieve_act_store_get_storage_error(aenv, trans);
+-              if (trans->error_code == MAIL_ERROR_EXISTS) {
+-                      trans->error = NULL;
+-                      trans->error_code = MAIL_ERROR_NONE;
+-              } else {
+-                      return (trans->error_code == MAIL_ERROR_TEMP ?
+-                              SIEVE_EXEC_TEMP_FAILURE : SIEVE_EXEC_FAILURE);
++              if (trans->error_code != MAIL_ERROR_EXISTS) {
++                      return sieve_act_store_create_error_status(
++                              aenv, trans->error_code);
+               }
++              /* Lost a race with another session; the mailbox exists now. */
++              trans->error = NULL;
++              trans->error_code = MAIL_ERROR_NONE;
+       }
+ 
+       /* Subscribe to it if necessary */
+@@ -402,10 +427,9 @@ int sieve_act_store_create_mailbox(const
+ 
+       /* Try opening again */
+       if (mailbox_open(box) < 0) {
+-              /* Failed definitively */
+               sieve_act_store_get_storage_error(aenv, trans);
+-              return (trans->error_code == MAIL_ERROR_TEMP ?
+-                      SIEVE_EXEC_TEMP_FAILURE : SIEVE_EXEC_FAILURE);
++              return sieve_act_store_create_error_status(
++                      aenv, trans->error_code);
+       }
+       return SIEVE_EXEC_OK;
+ }
+@@ -434,10 +458,6 @@ act_store_mailbox_alloc(const struct sie
+               return FALSE;
+       }
+ 
+-      if (eenv->scriptenv->mailbox_autocreate)
+-              flags |= MAILBOX_FLAG_AUTO_CREATE;
+-      if (eenv->scriptenv->mailbox_autosubscribe)
+-              flags |= MAILBOX_FLAG_AUTO_SUBSCRIBE;
+       *box_r = box = mailbox_alloc_for_user(eenv->scriptenv->user, mailbox,
+                                             flags);
+       *storage = mailbox_get_storage(box);
+@@ -640,6 +660,16 @@ act_store_execute(const struct sieve_act
+               }
+       }
+ 
++      /* Create the mailbox if it does not exist yet and autocreation is
++         enabled (e.g. lda_mailbox_autocreate=yes). The :create side effect
++         creates it earlier; this handles a plain fileinto. */
++      if (trans->error_code == MAIL_ERROR_NOTFOUND &&
++          eenv->scriptenv->mailbox_autocreate) {
++              int cstatus = sieve_act_store_create_mailbox(aenv, trans);
++              if (cstatus != SIEVE_EXEC_OK)
++                      return cstatus;
++      }
++
+       /* Exit early if transaction already failed */
+       switch (trans->error_code) {
+       case MAIL_ERROR_NONE:
+Index: trixie/pigeonhole/src/lib-sieve/sieve-actions.h
+===================================================================
+--- trixie.orig/pigeonhole/src/lib-sieve/sieve-actions.h
++++ trixie/pigeonhole/src/lib-sieve/sieve-actions.h
+@@ -246,8 +246,17 @@ void sieve_act_store_add_flags(const str
+ void sieve_act_store_get_storage_error(const struct sieve_action_exec_env *aenv,
+                                      struct act_store_transaction *trans);
+ 
++/* Map a failure to create the target mailbox to an execution status. A
++   permanent failure (e.g. the user lacks permission to create the mailbox)
++   makes a user's personal script fall through to implicit keep, but is
++   deferred for a global (administrator-defined) script, so that the
++   unintended behaviour is surfaced to the administrator. */
++int sieve_act_store_create_error_status(
++      const struct sieve_action_exec_env *aenv, enum mail_error error_code);
++
+ /* Create the target mailbox of the store transaction (and subscribe to it
+-   when configured). */
++   when configured). Returns SIEVE_EXEC_OK on success, or the status from
++   sieve_act_store_create_error_status() on failure. */
+ int sieve_act_store_create_mailbox(const struct sieve_action_exec_env *aenv,
+                                  struct act_store_transaction *trans);
+ 
diff --git a/patches/0002-lib-sieve-storage-file-Validate-script-stat-path-thr.patch b/patches/0002-lib-sieve-storage-file-Validate-script-stat-path-thr.patch
new file mode 100644 (file)
index 0000000..e459edd
--- /dev/null
@@ -0,0 +1,129 @@
+From 915cb2edc90da04d8a7a122fef7932cd55e3fff2 Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Mon, 4 May 2026 13:10:13 +0000
+Subject: [PATCH 2/2] lib-sieve: storage: file - Validate script stat path
+ through dir_fd
+
+Extend the symlink-escape protection added in the previous commit to the
+stat performed by sieve_file_script_open(): an "include :personal" lookup
+or any other indirect path that triggers sieve_file_script_stat() also
+needs to refuse a symlink whose target leaves the personal storage
+directory, otherwise the existence check succeeds and the file is opened
+later via the safe path with an unhelpful "permission denied".
+
+Add sieve_file_script_stat_safe(), which uses fstatat(AT_SYMLINK_NOFOLLOW)
+to obtain the entry's own stat (lnk_st) and then, only if the entry is a
+symlink, opens it through sieve_file_storage_open_safe() to validate the
+target stays inside dir_fd and to fetch the resolved target's stat (st)
+via fstat(). Non-symlink entries skip the open entirely.
+
+Use the new helper in sieve_file_script_open() whenever fstorage->dir_fd
+is available, falling back to the unsafe lstat+stat variant for
+non-personal or single-file storages.
+---
+ .../storage/file/sieve-file-script.c          | 78 ++++++++++++++++++-
+ 1 file changed, 75 insertions(+), 3 deletions(-)
+
+Index: trixie/pigeonhole/src/lib-sieve/storage/file/sieve-file-script.c
+===================================================================
+--- trixie.orig/pigeonhole/src/lib-sieve/storage/file/sieve-file-script.c
++++ trixie/pigeonhole/src/lib-sieve/storage/file/sieve-file-script.c
+@@ -285,6 +285,57 @@ sieve_file_script_stat(const char *path,
+       return 0;
+ }
+ 
++/* TOCTOU-safe variant of sieve_file_script_stat() for directory storages.
++   Resolves the script entry beneath fstorage->dir_fd, refusing symlinks
++   whose target escapes the storage directory. *st gets the (resolved)
++   target's stat; *lnk_st gets the directory entry's own stat (i.e.
++   AT_SYMLINK_NOFOLLOW), matching the semantics of the unsafe variant. */
++static int
++sieve_file_script_stat_safe(struct sieve_file_storage *fstorage,
++                          const char *filename, struct stat *st,
++                          struct stat *lnk_st, const char **error_r)
++{
++      int fd, saved_errno;
++
++      i_assert(fstorage->dir_fd >= 0);
++
++      if (fstatat(fstorage->dir_fd, filename, lnk_st,
++                  AT_SYMLINK_NOFOLLOW) < 0) {
++              *error_r = t_strdup_printf(
++                      "fstatat(%s/%s) failed: %m",
++                      fstorage->path, filename);
++              return -1;
++      }
++
++      if (!S_ISLNK(lnk_st->st_mode)) {
++              *st = *lnk_st;
++              return 0;
++      }
++
++      /* Symlink: open it via the safe walker (which refuses any target
++         that leaves fstorage->dir_fd) and read st from the resulting fd.
++         The fd is only used to fetch the resolved stat and is closed
++         immediately. Open with O_NONBLOCK so that a target which resolves
++         to a FIFO or other special file inside the storage directory only
++         yields its stat instead of blocking the delivery process. */
++      if (sieve_file_storage_open_safe(fstorage, filename,
++                                       O_RDONLY | O_NONBLOCK,
++                                       &fd, error_r) < 0)
++              return -1;
++
++      if (fstat(fd, st) < 0) {
++              saved_errno = errno;
++              *error_r = t_strdup_printf(
++                      "fstat() failed for '%s/%s': %m",
++                      fstorage->path, filename);
++              i_close_fd(&fd);
++              errno = saved_errno;
++              return -1;
++      }
++      i_close_fd(&fd);
++      return 0;
++}
++
+ static const char *
+ path_split_filename(const char *path, const char **dir_path_r)
+ {
+@@ -353,7 +404,25 @@ static int sieve_file_script_open(struct
+                               dir_path = path;
+ 
+                               path = sieve_file_storage_path_extend(fstorage, filename);
+-                              ret = sieve_file_script_stat(path, &st, &lnk_st);
++                              if (fstorage->dir_fd >= 0) {
++                                      const char *serror;
++
++                                      ret = sieve_file_script_stat_safe(
++                                              fstorage, filename, &st,
++                                              &lnk_st, &serror);
++                                      if (ret < 0 && errno == ELOOP) {
++                                              sieve_script_set_critical(
++                                                      script,
++                                                      "Failed to open sieve script: %s",
++                                                      serror);
++                                              script->storage->error_code =
++                                                      SIEVE_ERROR_NO_PERMISSION;
++                                              success = FALSE;
++                                      }
++                              } else {
++                                      ret = sieve_file_script_stat(
++                                              path, &st, &lnk_st);
++                              }
+                       }
+ 
+               } else {
+@@ -451,8 +520,11 @@ sieve_file_script_get_stream(struct siev
+ 
+       /* For directory-based storage, open the script via the storage
+          directory fd so that path resolution refuses to follow symlinks
+-         whose (recursive) target leaves the storage directory.
+-         Single-file storages have no dir_fd, so fall back to plain open(). */
++         whose (recursive) target leaves the storage directory. The fd-based
++         walk also makes this TOCTOU-safe: even if intermediate path
++         components are mutated between the stat in sieve_file_script_open()
++         and this call, the resolution stays within the original directory.
++         Single-file storages have no dir_fd, so fall back to a plain open(). */
+       if (fstorage->dir_fd >= 0 && fscript->filename != NULL &&
+           *fscript->filename != '\0') {
+               if (sieve_file_storage_open_safe(fstorage, fscript->filename,
diff --git a/patches/0002-lib-sieve-util-test-edit-mail-Adjust-to-changes-in-d.patch b/patches/0002-lib-sieve-util-test-edit-mail-Adjust-to-changes-in-d.patch
new file mode 100644 (file)
index 0000000..6fc833e
--- /dev/null
@@ -0,0 +1,104 @@
+From fe17ee9e58d2eb7dab38976518380279112c0bca Mon Sep 17 00:00:00 2001
+From: Stephan Bosch <stephan.bosch@open-xchange.com>
+Date: Wed, 29 Oct 2025 17:49:10 +0100
+Subject: [PATCH 2/3] lib-sieve: util: test-edit-mail - Adjust to changes in
+ dovecot core lib-test
+
+---
+ src/lib-sieve/util/test-edit-mail.c | 20 ++++++++++----------
+ 1 file changed, 10 insertions(+), 10 deletions(-)
+
+Index: trixie/pigeonhole/src/lib-sieve/util/test-edit-mail.c
+===================================================================
+--- trixie.orig/pigeonhole/src/lib-sieve/util/test-edit-mail.c
++++ trixie/pigeonhole/src/lib-sieve/util/test-edit-mail.c
+@@ -99,7 +99,7 @@ static void test_deinit_mail_user()
+               i_error("unlink_directory(%s) failed: %s", mail_home, error);
+ }
+ 
+-static void test_init(void)
++static void test_edit_mail_init(void)
+ {
+       test_pool = pool_alloconly_create(MEMPOOL_GROWING"test pool", 128);
+ 
+@@ -107,7 +107,7 @@ static void test_init(void)
+       test_raw_mail_user = mail_raw_user_create(test_mail_user);
+ }
+ 
+-static void test_deinit(void)
++static void test_edit_mail_deinit(void)
+ {
+       mail_user_unref(&test_raw_mail_user);
+       test_deinit_mail_user();
+@@ -186,7 +186,7 @@ static void test_edit_mail_concatenated(
+       const char *value;
+ 
+       test_begin("edit-mail - concatenated");
+-      test_init();
++      test_edit_mail_init();
+ 
+       /* Compose the message */
+ 
+@@ -434,7 +434,7 @@ static void test_edit_mail_concatenated(
+       edit_mail_unwrap(&edmail);
+       mail_raw_close(&rawmail);
+       i_stream_unref(&input_msg);
+-      test_deinit();
++      test_edit_mail_deinit();
+       test_end();
+ }
+ 
+@@ -696,7 +696,7 @@ static void test_edit_mail_big_header(vo
+       const char *value;
+ 
+       test_begin("edit-mail - big header");
+-      test_init();
++      test_edit_mail_init();
+ 
+       /* compose the message */
+ 
+@@ -737,7 +737,7 @@ static void test_edit_mail_big_header(vo
+       edit_mail_unwrap(&edmail);
+       mail_raw_close(&rawmail);
+       i_stream_unref(&input_msg);
+-      test_deinit();
++      test_edit_mail_deinit();
+       test_end();
+ }
+ 
+@@ -757,7 +757,7 @@ static void test_edit_mail_small_buffer(
+       unsigned int i;
+ 
+       test_begin("edit-mail - small buffer");
+-      test_init();
++      test_edit_mail_init();
+ 
+       /* compose the message */
+ 
+@@ -806,7 +806,7 @@ static void test_edit_mail_small_buffer(
+       edit_mail_unwrap(&edmail);
+       mail_raw_close(&rawmail);
+       i_stream_unref(&input_msg);
+-      test_deinit();
++      test_edit_mail_deinit();
+       test_end();
+ }
+ 
+@@ -820,7 +820,7 @@ static void test_edit_mail_empty(void)
+       const char *value;
+ 
+       test_begin("edit-mail - empty message");
+-      test_init();
++      test_edit_mail_init();
+ 
+       /* Compose the message */
+ 
+@@ -862,7 +862,7 @@ static void test_edit_mail_empty(void)
+       edit_mail_unwrap(&edmail);
+       mail_raw_close(&rawmail);
+       i_stream_unref(&input_msg);
+-      test_deinit();
++      test_edit_mail_deinit();
+       test_end();
+ }
+ 
diff --git a/patches/0002-lib-sql-Extract-sql_query_callback_delayed-helper.patch b/patches/0002-lib-sql-Extract-sql_query_callback_delayed-helper.patch
new file mode 100644 (file)
index 0000000..92e43fe
--- /dev/null
@@ -0,0 +1,53 @@
+From 9582563d2c261cda1cf7c59fb262292d1438cd73 Mon Sep 17 00:00:00 2001
+From: Aki Tuomi <aki.tuomi@open-xchange.com>
+Date: Fri, 29 May 2026 11:21:26 +0000
+Subject: [PATCH 2/6] lib-sql: Extract sql_query_callback_delayed() helper
+
+---
+ src/lib-sql/sql-api.c | 23 ++++++++++++++++-------
+ 1 file changed, 16 insertions(+), 7 deletions(-)
+
+diff --git a/src/lib-sql/sql-api.c b/src/lib-sql/sql-api.c
+index 4db6913229..5ee13f9b32 100644
+--- a/src/lib-sql/sql-api.c
++++ b/src/lib-sql/sql-api.c
+@@ -109,6 +109,20 @@ static struct sql_result *sql_result_new_error(const char *error)
+       return &result->result;
+ }
+ 
++static void
++sql_query_callback_delayed(struct sql_db *db, struct sql_result *result,
++                         sql_query_callback_t *callback, void *context)
++{
++      struct sql_query_result_delayed *cb =
++              i_new(struct sql_query_result_delayed, 1);
++      cb->db = db;
++      cb->result = result;
++      cb->callback = callback;
++      cb->context = context;
++      cb->to = timeout_add_short(0, sql_query_delayed_callback, cb);
++      DLLIST_PREPEND(&db->query_delayed_list, cb);
++}
++
+ void sql_drivers_init_without_drivers(void)
+ {
+       i_array_init(&sql_drivers, 8);
+@@ -347,13 +361,8 @@ void sql_query(struct sql_db *db, const char *query,
+               return;
+       }
+ 
+-      struct sql_query_result_delayed *cb = i_new(struct sql_query_result_delayed, 1);
+-      cb->db = db;
+-      cb->result = sql_query_s(db, query);
+-      cb->callback = callback;
+-      cb->context = context;
+-      cb->to = timeout_add_short(0, sql_query_delayed_callback, cb);
+-      DLLIST_PREPEND(&db->query_delayed_list, cb);
++      sql_query_callback_delayed(db, sql_query_s(db, query),
++                                 callback, context);
+ }
+ 
+ struct sql_result *sql_query_s(struct sql_db *db, const char *query)
+-- 
+2.39.5
+
diff --git a/patches/0002-lib-storage-Remove-MAILBOX_FLAG_AUTO_CREATE-and-MAIL.patch b/patches/0002-lib-storage-Remove-MAILBOX_FLAG_AUTO_CREATE-and-MAIL.patch
new file mode 100644 (file)
index 0000000..64dd4fd
--- /dev/null
@@ -0,0 +1,70 @@
+From e6c952b3ab417f97e9bbc3534b3e40131e808b1e Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Thu, 11 Jun 2026 17:20:40 +0000
+Subject: [PATCH 2/2] lib-storage: Remove MAILBOX_FLAG_AUTO_CREATE and
+ MAILBOX_FLAG_AUTO_SUBSCRIBE
+
+These flags were set only by mail delivery (lib-lda) and Sieve fileinto
+(Pigeonhole), both of which now create the target mailbox explicitly with
+mailbox_create(). Nothing sets the flags anymore, so remove them and drop
+the flag checks from mailbox_is_autocreated() and mailbox_is_autosubscribed().
+
+As a result mailbox_is_autocreated() reflects only mailboxes that virtually
+already exist (the user's INBOX and admin-configured "auto" mailboxes), so
+the ACL plugin again enforces the CREATE right for delivery-autocreated
+mailboxes in shared and public namespaces.
+---
+ src/lib-storage-lua/mail-storage-lua.c | 2 --
+ src/lib-storage/mail-storage.c         | 4 ----
+ src/lib-storage/mail-storage.h         | 4 ----
+ 3 files changed, 10 deletions(-)
+
+Index: trixie/src/lib-storage/mail-storage.c
+===================================================================
+--- trixie.orig/src/lib-storage/mail-storage.c
++++ trixie/src/lib-storage/mail-storage.c
+@@ -1375,16 +1375,12 @@ bool mailbox_is_autocreated(struct mailb
+ {
+       if (box->inbox_user)
+               return TRUE;
+-      if ((box->flags & MAILBOX_FLAG_AUTO_CREATE) != 0)
+-              return TRUE;
+       return box->set != NULL &&
+               strcmp(box->set->autocreate, MAILBOX_SET_AUTO_NO) != 0;
+ }
+ 
+ bool mailbox_is_autosubscribed(struct mailbox *box)
+ {
+-      if ((box->flags & MAILBOX_FLAG_AUTO_SUBSCRIBE) != 0)
+-              return TRUE;
+       return box->set != NULL &&
+               strcmp(box->set->autocreate, MAILBOX_SET_AUTO_SUBSCRIBE) == 0;
+ }
+Index: trixie/src/lib-storage/mail-storage.h
+===================================================================
+--- trixie.orig/src/lib-storage/mail-storage.h
++++ trixie/src/lib-storage/mail-storage.h
+@@ -74,10 +74,6 @@ enum mailbox_flags {
+          quota updates (possibly resulting in broken quota). and This is
+          useful for example when deleting entire user accounts. */
+       MAILBOX_FLAG_DELETE_UNSAFE      = 0x400,
+-      /* Mailbox is created implicitly if it does not exist. */
+-      MAILBOX_FLAG_AUTO_CREATE        = 0x1000,
+-      /* Mailbox is subscribed to implicitly when it is created automatically */
+-      MAILBOX_FLAG_AUTO_SUBSCRIBE     = 0x2000,
+       /* Run fsck for mailbox index before doing anything else. This may be
+          useful in fixing index corruption errors that aren't otherwise
+          detected and that are causing the full mailbox opening to fail. */
+Index: trixie/src/lib-storage/mail-storage-lua.c
+===================================================================
+--- trixie.orig/src/lib-storage/mail-storage-lua.c
++++ trixie/src/lib-storage/mail-storage-lua.c
+@@ -33,8 +33,6 @@ static struct dlua_table_values lua_stor
+       DLUA_TABLE_ENUM(MAILBOX_FLAG_NO_INDEX_FILES),
+       DLUA_TABLE_ENUM(MAILBOX_FLAG_KEEP_LOCKED),
+       DLUA_TABLE_ENUM(MAILBOX_FLAG_IGNORE_ACLS),
+-      DLUA_TABLE_ENUM(MAILBOX_FLAG_AUTO_CREATE),
+-      DLUA_TABLE_ENUM(MAILBOX_FLAG_AUTO_SUBSCRIBE),
+ 
+       DLUA_TABLE_ENUM(MAILBOX_SYNC_FLAG_FULL_READ),
+       DLUA_TABLE_ENUM(MAILBOX_SYNC_FLAG_FAST),
diff --git a/patches/0002-lib-storage-Simplify-mailbox_mkdir-using-mailbox_lis.patch b/patches/0002-lib-storage-Simplify-mailbox_mkdir-using-mailbox_lis.patch
new file mode 100644 (file)
index 0000000..de6e082
--- /dev/null
@@ -0,0 +1,31 @@
+From 01b4ddd1de4b5bfd499891f4b3592880b02c3854 Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Tue, 23 Jun 2026 15:26:51 +0000
+Subject: [PATCH 2/2] lib-storage: Simplify mailbox_mkdir() using
+ mailbox_list_mkdir_root(NULL)
+
+mailbox_mkdir() looked up the root path via mailbox_list_get_root_forced()
+only to pass it straight to mailbox_list_mkdir_root(). Let mkdir_root()
+resolve the root itself by passing path=NULL, dropping the local variable.
+---
+ src/lib-storage/mail-storage.c | 4 +---
+ 1 file changed, 1 insertion(+), 3 deletions(-)
+
+Index: trixie/src/lib-storage/mail-storage.c
+===================================================================
+--- trixie.orig/src/lib-storage/mail-storage.c
++++ trixie/src/lib-storage/mail-storage.c
+@@ -3441,12 +3441,10 @@ int mailbox_mkdir(struct mailbox *box, c
+                 enum mailbox_list_path_type type)
+ {
+       const struct mailbox_permissions *perm = mailbox_get_permissions(box);
+-      const char *root_dir;
+ 
+       if (!perm->gid_origin_is_mailbox_path) {
+               /* mailbox root directory doesn't exist, create it */
+-              root_dir = mailbox_list_get_root_forced(box->list, type);
+-              if (mailbox_list_mkdir_root(box->list, root_dir, type) < 0) {
++              if (mailbox_list_mkdir_root(box->list, NULL, type) < 0) {
+                       mail_storage_copy_list_error(box->storage, box->list);
+                       return -1;
+               }
diff --git a/patches/0002-lib-storage-thread-Limit-References-header-msgid-cou.patch b/patches/0002-lib-storage-thread-Limit-References-header-msgid-cou.patch
new file mode 100644 (file)
index 0000000..bbe38e1
--- /dev/null
@@ -0,0 +1,47 @@
+From fb14defe1804881a1df50c0b29679f9fdf8ff5b0 Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Thu, 16 Apr 2026 16:26:47 +0000
+Subject: [PATCH 2/3] lib-storage: thread - Limit References: header msgid
+ count to prevent O(N^2) CPU usage
+
+Cap per-message References ingestion at MAIL_THREAD_REFERENCES_MAX (1000)
+in mail_thread_map_add_mail(). Without this limit a single crafted email
+with N unique Message-IDs triggers N(N-1)/2 ancestor traversals in
+thread_node_has_ancestor(), allowing unauthenticated DoS via mail delivery.
+---
+ src/lib-storage/index/index-thread-private.h | 4 ++++
+ src/lib-storage/index/index-thread.c         | 3 ++-
+ 2 files changed, 6 insertions(+), 1 deletion(-)
+
+diff --git a/src/lib-storage/index/index-thread-private.h b/src/lib-storage/index/index-thread-private.h
+index ed2837b7bb..563c3a3d2d 100644
+--- a/src/lib-storage/index/index-thread-private.h
++++ b/src/lib-storage/index/index-thread-private.h
+@@ -22,6 +22,10 @@
+ #define MAIL_THREAD_NODE_REF_MSGID 0
+ #define MAIL_THREAD_NODE_REF_INREPLYTO 1
+ #define MAIL_THREAD_NODE_REF_REFERENCES1 2
++/* Maximum number of References: header message-IDs stored per message.
++   Prevents O(N²) CPU usage in thread_node_has_ancestor() when a single
++   crafted message contains an abnormally long reference chain. */
++#define MAIL_THREAD_REFERENCES_MAX 1000
+ 
+ struct mail_thread_node {
+       /* UID of the message, or 0 for dummy nodes */
+diff --git a/src/lib-storage/index/index-thread.c b/src/lib-storage/index/index-thread.c
+index e20a97072c..a58f9681f3 100644
+--- a/src/lib-storage/index/index-thread.c
++++ b/src/lib-storage/index/index-thread.c
+@@ -280,7 +280,8 @@ mail_thread_map_add_mail(struct mail_thread_context *ctx, struct mail *mail)
+                                               ref_index, msgid);
+                               }
+                       } T_END;
+-              } while (msgid != NULL);
++              } while (msgid != NULL &&
++                       ref_index < MAIL_THREAD_NODE_REF_REFERENCES1 + MAIL_THREAD_REFERENCES_MAX);
+       } else {
+               /* no References:, use In-Reply-To: */
+               if (thread_get_mail_header(mail, HDR_IN_REPLY_TO,
+-- 
+2.47.3
+
diff --git a/patches/0002-login-common-Avoid-array_front-panic-on-empty-forwar.patch b/patches/0002-login-common-Avoid-array_front-panic-on-empty-forwar.patch
new file mode 100644 (file)
index 0000000..36f45c6
--- /dev/null
@@ -0,0 +1,50 @@
+From 54211965c41eb15290bae017b8acfd95d08e0a9e Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Sun, 3 May 2026 16:24:38 +0000
+Subject: [PATCH 2/2] login-common: Avoid array_front() panic on empty
+ forward_fields array
+
+sasl_server_auth_request_info_fill() and proxy_redirect_reauth()
+NUL-terminate the forward_fields array via array_append_zero() +
+array_pop_back() and then call array_front() to hand the C array to
+the auth client. array_front() asserts when the array is empty, so a
+created-but-empty forward_fields array crashes login.
+
+The empty-array case is no longer reachable from
+client_forward_decode_base64() after the previous commit, but guard
+defensively here as well: any future caller that creates the array
+without populating it should not be able to panic the process.
+---
+ src/login-common/client-common-auth.c | 2 +-
+ src/login-common/sasl-server.c        | 2 +-
+ 2 files changed, 2 insertions(+), 2 deletions(-)
+
+diff --git a/src/login-common/client-common-auth.c b/src/login-common/client-common-auth.c
+index 0b226497c6..36ed7211a7 100644
+--- a/src/login-common/client-common-auth.c
++++ b/src/login-common/client-common-auth.c
+@@ -490,7 +490,7 @@ proxy_redirect_reauth(struct client *client, const char *destuser,
+       t_array_init(&info.extra_fields, N_ELEMENTS(extra_fields));
+       array_append(&info.extra_fields, extra_fields,
+                    N_ELEMENTS(extra_fields));
+-      if (array_is_created(&client->forward_fields)) {
++      if (array_not_empty(&client->forward_fields)) {
+               array_append_zero(&client->forward_fields);
+               array_pop_back(&client->forward_fields);
+               info.forward_fields = array_front(&client->forward_fields);
+diff --git a/src/login-common/sasl-server.c b/src/login-common/sasl-server.c
+index f02262c00f..88d46e7c68 100644
+--- a/src/login-common/sasl-server.c
++++ b/src/login-common/sasl-server.c
+@@ -534,7 +534,7 @@ int sasl_server_auth_request_info_fill(struct client *client,
+       info_r->real_remote_port = client->real_remote_port;
+       if (client->client_id != NULL)
+               info_r->client_id = str_c(client->client_id);
+-      if (array_is_created(&client->forward_fields)) {
++      if (array_not_empty(&client->forward_fields)) {
+               array_append_zero(&client->forward_fields);
+               array_pop_back(&client->forward_fields);
+               info_r->forward_fields = array_front(&client->forward_fields);
+-- 
+2.47.3
+
diff --git a/patches/0002-managesieve-login-client_skip_line-Discard-data-when.patch b/patches/0002-managesieve-login-client_skip_line-Discard-data-when.patch
new file mode 100644 (file)
index 0000000..22bd2b2
--- /dev/null
@@ -0,0 +1,29 @@
+From eee1ecfc512d348f8fb60633b6bd26b1536319da Mon Sep 17 00:00:00 2001
+From: Aki Tuomi <aki.tuomi@open-xchange.com>
+Date: Mon, 25 May 2026 10:38:13 +0000
+Subject: [PATCH 2/2] managesieve-login: client_skip_line() - Discard data when
+ newline is not found
+
+Without this, data without a newline (e.g., from a lone CR protocol violation
+detected by the parser) stays in the buffer indefinitely. This prevents the
+server from ever detecting client disconnect, causing a deadlock where both
+sides wait for the other.
+
+The post-login server-side client_skip_line() in managesieve-client.c already
+handles this correctly by always calling i_stream_skip().
+---
+ src/managesieve-login/client.c | 1 +
+ 1 file changed, 1 insertion(+)
+
+Index: trixie/pigeonhole/src/managesieve-login/client.c
+===================================================================
+--- trixie.orig/pigeonhole/src/managesieve-login/client.c
++++ trixie/pigeonhole/src/managesieve-login/client.c
+@@ -51,6 +51,7 @@ bool client_skip_line(struct managesieve
+               }
+       }
+ 
++      i_stream_skip(client->input, data_size);
+       return FALSE;
+ }
+ 
diff --git a/patches/0002-submission-login-client-Fix-panic-occurring-at-mail_.patch b/patches/0002-submission-login-client-Fix-panic-occurring-at-mail_.patch
new file mode 100644 (file)
index 0000000..a35ee92
--- /dev/null
@@ -0,0 +1,78 @@
+From f0bb034096518187b2b97ab25cd62a6d5517a150 Mon Sep 17 00:00:00 2001
+From: Stephan Bosch <stephan.bosch@open-xchange.com>
+Date: Fri, 28 Nov 2025 04:12:10 +0100
+Subject: [PATCH 2/5] submission-login: client - Fix panic occurring at
+ mail_max_userip_connections limit transgression
+
+Panic was:
+Panic: epoll_ctl(del, 8) failed: Bad file descriptor
+
+Fixed by making sure the underlying smtp-server connection is always closed before the connection FD is closed.
+---
+ src/lib-smtp/smtp-server-connection.c |  5 +++++
+ src/lib-smtp/smtp-server.h            |  1 +
+ src/submission-login/client.c         | 16 ++++++++++++++++
+ 3 files changed, 22 insertions(+)
+
+Index: trixie/src/lib-smtp/smtp-server-connection.c
+===================================================================
+--- trixie.orig/src/lib-smtp/smtp-server-connection.c
++++ trixie/src/lib-smtp/smtp-server-connection.c
+@@ -1401,6 +1401,11 @@ void smtp_server_connection_close(struct
+       smtp_server_connection_unref(&conn);
+ }
+ 
++bool smtp_server_connection_is_closed(struct smtp_server_connection *conn)
++{
++      return conn->closed;
++}
++
+ void smtp_server_connection_terminate(struct smtp_server_connection **_conn,
+                                     const char *enh_code, const char *reason)
+ {
+Index: trixie/src/lib-smtp/smtp-server.h
+===================================================================
+--- trixie.orig/src/lib-smtp/smtp-server.h
++++ trixie/src/lib-smtp/smtp-server.h
+@@ -476,6 +476,7 @@ void smtp_server_connection_set_ssl_stre
+ 
+ void smtp_server_connection_close(struct smtp_server_connection **_conn,
+                                 const char *reason) ATTR_NULL(2);
++bool smtp_server_connection_is_closed(struct smtp_server_connection *conn);
+ void smtp_server_connection_terminate(struct smtp_server_connection **_conn,
+                                     const char *enh_code, const char *reason)
+                                     ATTR_NULL(3);
+Index: trixie/src/submission-login/client.c
+===================================================================
+--- trixie.orig/src/submission-login/client.c
++++ trixie/src/submission-login/client.c
+@@ -146,6 +146,21 @@ static int submission_client_create(stru
+       return 0;
+ }
+ 
++static void
++submission_client_disconnect(struct client *client, const char *reason)
++{
++      struct submission_client *subm_client =
++              container_of(client, struct submission_client, common);
++
++      /* If the smtp-server connection is already in its close cascade (i.e.
++         we're being called via its conn_disconnect callback), skip closing
++         it again. Doing so would null subm_client->conn and prevent the
++         conn_free callback from invoking client_destroy(). */
++      if (subm_client->conn != NULL &&
++          !smtp_server_connection_is_closed(subm_client->conn))
++              smtp_server_connection_close(&subm_client->conn, reason);
++}
++
+ static void submission_client_destroy(struct client *client)
+ {
+       struct submission_client *subm_client =
+@@ -320,6 +335,7 @@ static const struct smtp_server_callback
+ static struct client_vfuncs submission_client_vfuncs = {
+       .alloc = submission_client_alloc,
+       .create = submission_client_create,
++      .disconnect = submission_client_disconnect,
+       .destroy = submission_client_destroy,
+       .reload_config = submission_client_reload_config,
+       .notify_auth_ready = submission_client_notify_auth_ready,
diff --git a/patches/0003-auth-db-oauth2-Add-oauth2_audience-setting-deprecate.patch b/patches/0003-auth-db-oauth2-Add-oauth2_audience-setting-deprecate.patch
new file mode 100644 (file)
index 0000000..8ef24a4
--- /dev/null
@@ -0,0 +1,131 @@
+From fd7d4fddf1798b118b0c5481a0dabe03250829ca Mon Sep 17 00:00:00 2001
+From: Aki Tuomi <aki.tuomi@open-xchange.com>
+Date: Fri, 29 May 2026 07:31:50 +0000
+Subject: [PATCH 3/4] auth: db-oauth2: Add oauth2_audience setting, deprecate
+ aud-as-scope fallback
+
+Add a dedicated oauth2_audience setting checked against the token's aud
+claim (RFC 7519 section 4.1.3) using the same tab-split AND-semantics as
+oauth2_scope. Emit a deprecation warning when the existing aud fallback
+in db_oauth2_token_in_scope() is triggered so operators know to migrate.
+---
+ src/auth/db-oauth2.c | 53 ++++++++++++++++++++++++++++++++++++++++++--
+ src/auth/db-oauth2.h |  2 ++
+ 2 files changed, 53 insertions(+), 2 deletions(-)
+
+Index: trixie/src/auth/db-oauth2.c
+===================================================================
+--- trixie.orig/src/auth/db-oauth2.c
++++ trixie/src/auth/db-oauth2.c
+@@ -17,16 +17,21 @@
+ #include "db-oauth2.h"
+ #include "dcrypt.h"
+ #include "dict.h"
++#include <time.h>
+ 
+ #undef DEF
+ #define DEF(type, name) \
+       SETTING_DEFINE_STRUCT_##type("oauth2_"#name, name, struct auth_oauth2_settings)
+ 
++#define WARN_AUD_FALLBACK_PERIOD 600
++static time_t last_warned_aud_fallback = 0;
++
+ static const struct setting_define auth_oauth2_setting_defines[] = {
+       DEF(STR, tokeninfo_url),
+       DEF(STR, grant_url),
+       DEF(STR, introspection_url),
+       DEF(BOOLLIST, scope),
++      DEF(BOOLLIST, audience),
+       DEF(ENUM, introspection_mode),
+       DEF(STR_NOVARS, username_validation_format),
+       DEF(STR, username_attribute),
+@@ -50,6 +55,7 @@ static const struct auth_oauth2_settings
+       .grant_url = "",
+       .introspection_url = "",
+       .scope = ARRAY_INIT,
++      .audience = ARRAY_INIT,
+       .force_introspection = FALSE,
+       .introspection_mode = ":auth:get:post:local",
+       .username_validation_format = "%{user}",
+@@ -558,8 +564,19 @@ db_oauth2_token_in_scope(struct db_oauth
+ 
+       const char *value = auth_fields_find(req->fields, "scope");
+       bool has_scope = value != NULL;
+-      if (!has_scope)
++      if (!has_scope && array_is_empty(&req->db->set->audience)) {
+               value = auth_fields_find(req->fields, "aud");
++              if (value != NULL) {
++                      time_t t0 = time(NULL);
++                      if (t0 - last_warned_aud_fallback > WARN_AUD_FALLBACK_PERIOD) {
++                              e_warning(authdb_event(req->auth_request),
++                                        "Token has no 'scope' claim; falling back to "
++                                        "'aud' for scope check is deprecated - "
++                                        "use oauth2_audience instead");
++                              last_warned_aud_fallback = t0;
++                      }
++              }
++      }
+       e_debug(authdb_event(req->auth_request),
+               "Token scope(s): %s", value);
+ 
+@@ -585,6 +602,37 @@ db_oauth2_token_in_scope(struct db_oauth
+       return found;
+ }
+ 
++static bool
++db_oauth2_token_in_audience(struct db_oauth2_request *req,
++                          enum passdb_result *result_r, const char **error_r)
++{
++      if (array_is_empty(&req->db->set->audience))
++              return TRUE;
++
++      const char *value = auth_fields_find(req->fields, "aud");
++      e_debug(authdb_event(req->auth_request),
++              "Token audience(s): %s", value != NULL ? value : "(none)");
++
++      bool found = FALSE;
++      if (value != NULL && *value != '\0') {
++              const char *const *entries = t_strsplit_tabescaped(value);
++              const char *wanted;
++              found = TRUE;
++              array_foreach_elem(&req->db->set->audience, wanted) {
++                      if (!str_array_find(entries, wanted)) {
++                              found = FALSE;
++                              break;
++                      }
++              }
++      }
++      if (!found) {
++              *error_r = t_strdup_printf("Token audience does not include '%s'",
++                      t_array_const_string_join(&req->db->set->audience, " "));
++              *result_r = PASSDB_RESULT_USER_DISABLED;
++      }
++      return found;
++}
++
+ static void db_oauth2_process_fields(struct db_oauth2_request *req,
+                                    enum passdb_result *result_r,
+                                    const char **error_r)
+@@ -593,7 +641,8 @@ static void db_oauth2_process_fields(str
+ 
+       if (db_oauth2_user_is_enabled(req, result_r, error_r) &&
+           db_oauth2_validate_username(req, result_r, error_r) &&
+-          db_oauth2_token_in_scope(req, result_r, error_r)) {
++          db_oauth2_token_in_scope(req, result_r, error_r) &&
++          db_oauth2_token_in_audience(req, result_r, error_r)) {
+               /* The user has now been successfully authenticated,
+                  mark the request as such. This allows having no
+                  passdb in config. */
+Index: trixie/src/auth/db-oauth2.h
+===================================================================
+--- trixie.orig/src/auth/db-oauth2.h
++++ trixie/src/auth/db-oauth2.h
+@@ -15,6 +15,8 @@ struct auth_oauth2_settings {
+       const char *introspection_url;
+       /* expected scope(s), optional */
+       ARRAY_TYPE(const_string) scope;
++      /* expected audience(s) (aud claim), optional */
++      ARRAY_TYPE(const_string) audience;
+       /* mode of introspection, one of auth, get, post, local
+          - auth: send token with header Authorization: Bearer token
+          - get: append token to url
diff --git a/patches/0003-doveadm-compress-Delay-failing-if-used-COMPRESS-algo.patch b/patches/0003-doveadm-compress-Delay-failing-if-used-COMPRESS-algo.patch
new file mode 100644 (file)
index 0000000..f34397f
--- /dev/null
@@ -0,0 +1,41 @@
+From 161256a377c194aa8e63c83053c35cdd1ab75eda Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Tue, 7 Jul 2026 14:33:40 +0300
+Subject: [PATCH 3/3] doveadm compress: Delay failing if used COMPRESS
+ algorithm is not supported locally
+
+This command is used in CI tests to verify the server rejects non-DEFLATE
+algorithm.
+---
+ src/doveadm/doveadm-compress.c | 9 ++++++++-
+ 1 file changed, 8 insertions(+), 1 deletion(-)
+
+Index: trixie/src/doveadm/doveadm-compress.c
+===================================================================
+--- trixie.orig/src/doveadm/doveadm-compress.c
++++ trixie/src/doveadm/doveadm-compress.c
+@@ -123,9 +123,14 @@ client_input_get_compress_algorithm(stru
+       if (!str_begins_icase(line, " COMPRESS ", &algorithm))
+               return FALSE;
+ 
++      /* Look up the local handler needed to (de)compress the stream once the
++         server accepts. Don't fail here if the mechanism is unknown or not
++         compiled in: the server may reject it, and this client must still send
++         the command to let the server do so. If the server unexpectedly
++         accepts a mechanism we can't handle, we fail then (see server_input()). */
+       if (compression_lookup_handler(t_str_lcase(algorithm),
+                                      &client->handler) <= 0)
+-              i_fatal("Unsupported compression mechanism: %s", algorithm);
++              client->handler = NULL;
+       /* Remember the tag so we can tell whether the server accepted or
+          rejected this COMPRESS command. */
+       i_free(client->compress_tag);
+@@ -281,6 +286,8 @@ static void server_input(struct client *
+                       struct istream *input;
+                       struct ostream *output;
+ 
++                      if (client->handler == NULL)
++                              i_fatal("Server accepted unsupported compression mechanism");
+                       e_info(client->event, "<Compression started>");
+                       input = client->handler->create_istream(client->input);
+                       output = client->handler->
diff --git a/patches/0003-imap-Stream-ENVELOPE-to-client-without-ostream-memor.patch b/patches/0003-imap-Stream-ENVELOPE-to-client-without-ostream-memor.patch
new file mode 100644 (file)
index 0000000..aafce22
--- /dev/null
@@ -0,0 +1,75 @@
+From e14d6ae2825adf24f58e397e3cf88d8c9f41e8a2 Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Fri, 17 Apr 2026 16:55:34 +0200
+Subject: [PATCH 03/14] imap: Stream ENVELOPE to client without ostream memory
+ duplication
+
+Same fix as for BODYSTRUCTURE: avoid copying the envelope string into
+the ostream ring buffer by using o_stream_set_max_buffer_size(0) +
+o_stream_send_istream() with a continuation handler.
+
+Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
+---
+ src/imap/imap-fetch.c | 38 ++++++++++++++++++++++++++++++++++----
+ 1 file changed, 34 insertions(+), 4 deletions(-)
+
+diff --git a/src/imap/imap-fetch.c b/src/imap/imap-fetch.c
+index b4a8a465d7..5a6629916b 100644
+--- a/src/imap/imap-fetch.c
++++ b/src/imap/imap-fetch.c
+@@ -793,6 +793,34 @@ static bool fetch_bodystructure_init(struct imap_fetch_init_context *ctx)
+       return TRUE;
+ }
+ 
++static int fetch_envelope_stream_continue(struct imap_fetch_context *ctx)
++{
++      struct imap_fetch_state *state = &ctx->state;
++      enum ostream_send_istream_result res;
++
++      o_stream_set_max_buffer_size(ctx->client->output, 0);
++      res = o_stream_send_istream(ctx->client->output, state->cur_input);
++      o_stream_set_max_buffer_size(ctx->client->output, SIZE_MAX);
++
++      switch (res) {
++      case OSTREAM_SEND_ISTREAM_RESULT_FINISHED:
++              i_stream_unref(&state->cur_input);
++              state->cont_handler = NULL;
++              if (o_stream_send(ctx->client->output, ")", 1) < 0)
++                      return -1;
++              return 1;
++      case OSTREAM_SEND_ISTREAM_RESULT_WAIT_INPUT:
++              i_unreached();
++      case OSTREAM_SEND_ISTREAM_RESULT_WAIT_OUTPUT:
++              return 0;
++      case OSTREAM_SEND_ISTREAM_RESULT_ERROR_INPUT:
++              i_unreached();
++      case OSTREAM_SEND_ISTREAM_RESULT_ERROR_OUTPUT:
++              return -1;
++      }
++      i_unreached();
++}
++
+ static int fetch_envelope(struct imap_fetch_context *ctx, struct mail *mail,
+                         void *context ATTR_UNUSED)
+ {
+@@ -808,11 +836,13 @@ static int fetch_envelope(struct imap_fetch_context *ctx, struct mail *mail,
+                       return -1;
+       }
+ 
+-      if (o_stream_send(ctx->client->output, "ENVELOPE (", 10) < 0 ||
+-          o_stream_send_str(ctx->client->output, envelope) < 0 ||
+-          o_stream_send(ctx->client->output, ")", 1) < 0)
++      if (o_stream_send(ctx->client->output, "ENVELOPE (", 10) < 0)
+               return -1;
+-      return 1;
++
++      ctx->state.cur_input =
++              i_stream_create_from_data(envelope, strlen(envelope));
++      ctx->state.cont_handler = fetch_envelope_stream_continue;
++      return fetch_envelope_stream_continue(ctx);
+ }
+ 
+ static bool fetch_envelope_init(struct imap_fetch_init_context *ctx)
+-- 
+2.47.3
+
diff --git a/patches/0003-lib-Add-str_stable_hash.patch b/patches/0003-lib-Add-str_stable_hash.patch
new file mode 100644 (file)
index 0000000..a807e27
--- /dev/null
@@ -0,0 +1,43 @@
+From 4778fe2357a1fcaa45d566f5095e80d8be8f4e4b Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Mon, 13 Apr 2026 17:20:46 +0200
+Subject: [PATCH 03/12] lib: Add str_stable_hash()
+
+For now this is the same as str_hash(), but this is changed in a following
+commit.
+---
+ src/lib/hash.c | 5 +++++
+ src/lib/hash.h | 1 +
+ 2 files changed, 6 insertions(+)
+
+diff --git a/src/lib/hash.c b/src/lib/hash.c
+index 8f4cd7d9b4..61fe86ed8e 100644
+--- a/src/lib/hash.c
++++ b/src/lib/hash.c
+@@ -525,6 +525,11 @@ unsigned int str_hash(const char *p)
+       return xxh64_to_32(xxh64_data(p, strlen(p), 0));
+ }
+ 
++unsigned int str_stable_hash(const char *p)
++{
++      return xxh64_to_32(xxh64_data(p, strlen(p), 0));
++}
++
+ unsigned int strcase_hash(const char *p)
+ {
+       struct xxh64_context ctx;
+diff --git a/src/lib/hash.h b/src/lib/hash.h
+index 5716a77a7d..84d9c52aef 100644
+--- a/src/lib/hash.h
++++ b/src/lib/hash.h
+@@ -171,6 +171,7 @@ void hash_table_copy(struct hash_table *dest, struct hash_table *src);
+ /* hash function for strings */
+ unsigned int str_hash(const char *p) ATTR_PURE;
+ unsigned int strcase_hash(const char *p) ATTR_PURE;
++unsigned int str_stable_hash(const char *p) ATTR_PURE;
+ 
+ /* fast hash function which uppercases a-z. Does not work well
+    with input that consists from non number/letter input, as
+-- 
+2.47.3
+
diff --git a/patches/0003-lib-Add-t_openat_safe_dir.patch b/patches/0003-lib-Add-t_openat_safe_dir.patch
new file mode 100644 (file)
index 0000000..63cc4fe
--- /dev/null
@@ -0,0 +1,129 @@
+From ba9ae23ee80dece8ebc3bb6b28c42b35002afb96 Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Wed, 3 Jun 2026 18:10:36 +0300
+Subject: [PATCH 3/5] lib: Add t_openat_safe_dir()
+
+---
+ src/lib/path-util.c      | 19 +++++++++++++++
+ src/lib/path-util.h      |  7 ++++++
+ src/lib/test-path-util.c | 52 ++++++++++++++++++++++++++++++++++++++++
+ 3 files changed, 78 insertions(+)
+
+Index: trixie/src/lib/path-util.c
+===================================================================
+--- trixie.orig/src/lib/path-util.c
++++ trixie/src/lib/path-util.c
+@@ -528,6 +528,25 @@ int t_openat_safe(int base_fd, const cha
+       return path_openat_safe_walk(base_fd, path, flags, error_r);
+ }
+ 
++int t_openat_safe_dir(const char *base_dir, const char *path, int flags,
++                    const char **error_r)
++{
++      i_assert(base_dir != NULL);
++      i_assert(path != NULL);
++      i_assert(error_r != NULL);
++
++      int dir_fd = open(base_dir,
++                        O_DIRECTORY | O_RDONLY | O_CLOEXEC |
++                        (flags & O_NOFOLLOW));
++      if (dir_fd < 0) {
++              *error_r = t_strdup_printf("open(%s) failed: %m", base_dir);
++              return -1;
++      }
++      int fd = t_openat_safe(dir_fd, path, flags, error_r);
++      i_close_fd(&dir_fd);
++      return fd;
++}
++
+ bool t_binary_abspath(const char **binpath, const char **error_r)
+ {
+       const char *path_env, *const *paths;
+Index: trixie/src/lib/path-util.h
+===================================================================
+--- trixie.orig/src/lib/path-util.h
++++ trixie/src/lib/path-util.h
+@@ -86,6 +86,13 @@ int t_readlink(const char *path, const c
+ int t_openat_safe(int base_fd, const char *path, int flags,
+                 const char **error_r);
+ 
++/* Convenience wrapper: opens base_dir as a directory fd, then delegates to
++   t_openat_safe(). If O_NOFOLLOW is present in flags it is also applied to
++   the open() of base_dir itself, refusing a symlink as its final component.
++   Returns the new fd on success, -1 on failure. */
++int t_openat_safe_dir(const char *base_dir, const char *path, int flags,
++                    const char **error_r);
++
+ /* Update binpath to be absolute:
+  * a) begins with '/' -> no change
+  * b) contains '/' -> assume relative to working directory
+Index: trixie/src/lib/test-path-util.c
+===================================================================
+--- trixie.orig/src/lib/test-path-util.c
++++ trixie/src/lib/test-path-util.c
+@@ -356,6 +356,57 @@ static void test_openat_safe(void)
+               i_error("rmdir(%s) failed: %m", subdir);
+ }
+ 
++static void test_openat_safe_dir(void)
++{
++      const char *error;
++      int fd;
++
++      /* Plain file: success. */
++      const char *plain = t_strconcat(tmpdir, "/plain2", NULL);
++      int wfd = creat(plain, 0600);
++      if (wfd < 0)
++              i_fatal("creat(%s) failed: %m", plain);
++      i_close_fd(&wfd);
++
++      fd = t_openat_safe_dir(tmpdir, "plain2", O_RDONLY, &error);
++      test_assert(fd >= 0);
++      i_close_fd(&fd);
++
++      /* Nonexistent base_dir. */
++      errno = 0;
++      fd = t_openat_safe_dir(t_strconcat(tmpdir, "/no-such-dir", NULL),
++                             "plain2", O_RDONLY, &error);
++      test_assert(fd == -1);
++      test_assert(errno == ENOENT);
++      test_assert(error != NULL);
++
++      /* Absolute symlink in relative part: refused. */
++      const char *abs_link2 = t_strconcat(tmpdir, "/abs-link2", NULL);
++      if (symlink("/etc/hostname", abs_link2) < 0)
++              i_fatal("symlink failed: %m");
++      errno = 0;
++      fd = t_openat_safe_dir(tmpdir, "abs-link2", O_RDONLY, &error);
++      test_assert(fd == -1);
++      test_assert(errno == ELOOP);
++
++      /* O_NOFOLLOW: symlink as base_dir final component refused. */
++      const char *dir_link = t_strconcat(tmpdir, "/dir-link", NULL);
++      if (symlink(tmpdir, dir_link) < 0)
++              i_fatal("symlink failed: %m");
++      errno = 0;
++      fd = t_openat_safe_dir(dir_link, "plain2",
++                             O_RDONLY | O_NOFOLLOW, &error);
++      test_assert(fd == -1);
++      /* Without O_NOFOLLOW the same symlinked base_dir succeeds. */
++      fd = t_openat_safe_dir(dir_link, "plain2", O_RDONLY, &error);
++      test_assert(fd >= 0);
++      i_close_fd(&fd);
++
++      i_unlink(plain);
++      i_unlink(abs_link2);
++      i_unlink(dir_link);
++}
++
+ static void test_cleanup(void)
+ {
+       const char *error;
+@@ -396,6 +447,7 @@ void test_path_util(void)
+       test_link_alloc();
+       test_link_alloc2();
+       test_openat_safe();
++      test_openat_safe_dir();
+       test_cleanup();
+       alarm(0);
+       test_end();
diff --git a/patches/0003-lib-compression-Add-o_stream_deflate_reset_dict.patch b/patches/0003-lib-compression-Add-o_stream_deflate_reset_dict.patch
new file mode 100644 (file)
index 0000000..f9e2654
--- /dev/null
@@ -0,0 +1,104 @@
+From c7629782bfbb0f32076c220230db67bd01d5e115 Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Thu, 16 Apr 2026 17:38:41 +0200
+Subject: [PATCH 3/5] lib-compression: Add o_stream_deflate_reset_dict()
+
+New public function that locates the deflate ostream in the parent chain
+(transparently handles rawlog wrappers) and schedules a Z_FULL_FLUSH on
+the next uncork/flush.  Z_FULL_FLUSH emits all buffered data and then
+resets the deflate dictionary, so subsequent compressed output cannot
+reference data from before the call.
+
+Adds a pending_dict_reset flag to struct zlib_ostream.  The flag persists
+through partial flush retries and is cleared only once the full flush
+loop completes.
+---
+ src/lib-compression/ostream-zlib.c | 37 ++++++++++++++++++++++++++++--
+ src/lib-compression/ostream-zlib.h |  7 ++++++
+ 2 files changed, 42 insertions(+), 2 deletions(-)
+
+diff --git a/src/lib-compression/ostream-zlib.c b/src/lib-compression/ostream-zlib.c
+index 6aeccbf35d..5bc41d0b2c 100644
+--- a/src/lib-compression/ostream-zlib.c
++++ b/src/lib-compression/ostream-zlib.c
+@@ -23,6 +23,7 @@ struct zlib_ostream {
+ 
+       bool gz:1;
+       bool flushed:1;
++      bool pending_dict_reset:1;
+ };
+ 
+ struct zlib_settings {
+@@ -252,8 +253,14 @@ o_stream_zlib_send_flush(struct zlib_ostream *zstream, bool final)
+       if ((ret = o_stream_zlib_send_outbuf(zstream)) <= 0)
+               return ret;
+ 
+-      flush = final ? Z_FINISH :
+-              (!zstream->gz ? Z_SYNC_FLUSH : Z_NO_FLUSH);
++      if (final)
++              flush = Z_FINISH;
++      else if (zstream->gz)
++              flush = Z_NO_FLUSH;
++      else if (zstream->pending_dict_reset)
++              flush = Z_FULL_FLUSH;
++      else
++              flush = Z_SYNC_FLUSH;
+ 
+       i_assert(zstream->outbuf_used == 0);
+       do {
+@@ -283,6 +290,10 @@ o_stream_zlib_send_flush(struct zlib_ostream *zstream, bool final)
+               }
+       } while (zs->avail_out != sizeof(zstream->outbuf));
+ 
++      /* Z_FULL_FLUSH completes here; clear the request so subsequent flushes
++         use the normal Z_SYNC_FLUSH again. */
++      zstream->pending_dict_reset = FALSE;
++
+       if (final) {
+               if (o_stream_zlib_send_gz_trailer(zstream) < 0)
+                       return -1;
+@@ -443,3 +454,25 @@ struct ostream *o_stream_create_deflate_auto(struct ostream *output, struct even
+ {
+       return o_stream_create_zlib_auto(output, event, FALSE);
+ }
++void o_stream_deflate_reset_dict(struct ostream *_output)
++{
++      struct ostream_private *stream = _output->real_stream;
++
++      /* Traverse the ostream parent chain to find the deflate ostream.
++         The caller may pass a rawlog-wrapped stream or the deflate stream
++         itself; either way we find it by its sendv function pointer. */
++      while (stream != NULL) {
++              if (stream->sendv == o_stream_zlib_sendv) {
++                      struct zlib_ostream *zstream =
++                              container_of(stream, struct zlib_ostream, ostream);
++                      i_assert(!zstream->gz);
++                      zstream->pending_dict_reset = TRUE;
++                      o_stream_set_flush_pending(
++                              &zstream->ostream.ostream, TRUE);
++                      return;
++              }
++              stream = (stream->parent != NULL) ?
++                      stream->parent->real_stream : NULL;
++      }
++      /* No deflate ostream found in the chain â€“ nothing to reset. */
++}
+diff --git a/src/lib-compression/ostream-zlib.h b/src/lib-compression/ostream-zlib.h
+index 9738e36cc0..4db62fda81 100644
+--- a/src/lib-compression/ostream-zlib.h
++++ b/src/lib-compression/ostream-zlib.h
+@@ -7,4 +7,11 @@ struct ostream *o_stream_create_bz2_auto(struct ostream *output, struct event *e
+ struct ostream *o_stream_create_lz4_auto(struct ostream *output, struct event *event);
+ struct ostream *o_stream_create_zstd_auto(struct ostream *output, struct event *event);
+ 
++/* Reset the deflate compression dictionary so that subsequent compressed
++   output cannot reference data from before this call.  Pass any ostream in
++   the chain (e.g. a rawlog wrapper); the function locates the deflate stream
++   internally.  Safe to call when compression is not active â€“ it is a no-op
++   in that case. */
++void o_stream_deflate_reset_dict(struct ostream *output);
++
+ #endif
+-- 
+2.47.3
+
diff --git a/patches/0003-lib-compression-istream-zstd-Guard-against-no-progre.patch b/patches/0003-lib-compression-istream-zstd-Guard-against-no-progre.patch
new file mode 100644 (file)
index 0000000..e4fdefa
--- /dev/null
@@ -0,0 +1,39 @@
+From cc00fbd91027e9f7b3595ced4c505deec33e7f6c Mon Sep 17 00:00:00 2001
+From: Aki Tuomi <aki.tuomi@open-xchange.com>
+Date: Fri, 12 Jun 2026 11:19:42 +0000
+Subject: [PATCH 3/4] lib-compression: istream-zstd - Guard against no-progress
+ loop in read
+
+If ZSTD_decompressStream() returns with neither input consumed nor
+output produced, the read loop would spin indefinitely. Add a check
+after each call: if input.pos is unchanged and output.pos is zero,
+treat it as a corrupt stream (EIO).
+---
+ src/lib-compression/istream-zstd.c | 8 ++++++++
+ 1 file changed, 8 insertions(+)
+
+Index: trixie/src/lib-compression/istream-zstd.c
+===================================================================
+--- trixie.orig/src/lib-compression/istream-zstd.c
++++ trixie/src/lib-compression/istream-zstd.c
+@@ -175,12 +175,20 @@ static ssize_t i_stream_zstd_read(struct
+               zstream->output.pos = 0;
+               zstream->output.size = ZSTD_DStreamOutSize();
+ 
++              size_t old_input_pos = zstream->input.pos;
+               size_t zret = ZSTD_decompressStream(zstream->dstream, &zstream->output,
+                                                   &zstream->input);
+               if (ZSTD_isError(zret) != 0) {
+                       i_stream_zstd_read_error(zstream, zret);
+                       return -1;
+               }
++              if (zstream->input.pos == old_input_pos && zstream->output.pos == 0) {
++                      io_stream_set_error(&zstream->istream.iostream,
++                                          "zstd.read(%s): decompressor made no progress",
++                                          i_stream_get_name(&zstream->istream.istream));
++                      zstream->istream.istream.stream_errno = EIO;
++                      return -1;
++              }
+               /* ZSTD magic number is 4 bytes, but it's only defined after v0.8 */
+               if (!zstream->hdr_read && zstream->input.size > 4)
+                       zstream->hdr_read = TRUE;
diff --git a/patches/0003-lib-mail-o_stream_dot_sendv-Use-enumeration-to-defin.patch b/patches/0003-lib-mail-o_stream_dot_sendv-Use-enumeration-to-defin.patch
new file mode 100644 (file)
index 0000000..ecf30b1
--- /dev/null
@@ -0,0 +1,91 @@
+From a26d37e321c46eb974703ac85bab66cce544d35f Mon Sep 17 00:00:00 2001
+From: Marco Bettini <marco.bettini@open-xchange.com>
+Date: Fri, 10 Apr 2026 08:49:49 +0000
+Subject: [PATCH 3/4] lib-mail: o_stream_dot_sendv() - Use enumeration to
+ define the items to inject
+
+---
+ src/lib-mail/ostream-dot.c | 39 ++++++++++++++++++++++++++------------
+ 1 file changed, 27 insertions(+), 12 deletions(-)
+
+Index: trixie/src/lib-mail/ostream-dot.c
+===================================================================
+--- trixie.orig/src/lib-mail/ostream-dot.c
++++ trixie/src/lib-mail/ostream-dot.c
+@@ -71,6 +71,12 @@ o_stream_dot_close(struct iostream_priva
+               o_stream_close(dstream->ostream.parent);
+ }
+ 
++enum o_stream_dot_sendv_add {
++      ADD_NONE,
++      ADD_CR,
++      ADD_DOT,
++};
++
+ static ssize_t
+ o_stream_dot_sendv(struct ostream_private *stream,
+                   const struct const_iovec *iov, unsigned int iov_count)
+@@ -106,7 +112,7 @@ o_stream_dot_sendv(struct ostream_privat
+               p = data;
+               pend = CONST_PTR_OFFSET(data, size);
+               for (; p < pend && ((size_t)(p - data) + 2) <= max_bytes; p++) {
+-                      char add = 0;
++                      enum o_stream_dot_sendv_add add = ADD_NONE;
+ 
+                       size = pend - p;
+                       switch (dstream->state) {
+@@ -122,8 +128,7 @@ o_stream_dot_sendv(struct ostream_privat
+                               switch (*p) {
+                               case '\n':
+                                       dstream->state = STREAM_STATE_CRLF;
+-                                      /* add missing CR */
+-                                      add = '\r';
++                                      add = ADD_CR;
+                                       break;
+                               case '\r':
+                                       dstream->state = STREAM_STATE_CR;
+@@ -155,12 +160,10 @@ o_stream_dot_sendv(struct ostream_privat
+                                       break;
+                               case '\n':
+                                       dstream->state = STREAM_STATE_CRLF;
+-                                      /* add missing CR */
+-                                      add = '\r';
++                                      add = ADD_CR;
+                                       break;
+                               case '.':
+-                                      /* add dot */
+-                                      add = '.';
++                                      add = ADD_DOT;
+                                       /* fall through */
+                               default:
+                                       dstream->state = STREAM_STATE_NONE;
+@@ -185,12 +188,24 @@ o_stream_dot_sendv(struct ostream_privat
+                               }
+                               /* insert byte (substitute one with pair) */
+                               data++;
+-                              iovn.iov_base = (add == '\r' ? "\r\n" : "..");
+-                              iovn.iov_len = 2;
++
++                              switch(add) {
++                              case ADD_DOT:
++                                      iovn.iov_base = "..";
++                                      iovn.iov_len = 2;
++                                      break;
++                              case ADD_CR:
++                                      iovn.iov_base = "\r\n";
++                                      iovn.iov_len = 2;
++                                      break;
++                              default:
++                                      i_unreached();
++                              }
++
+                               array_push_back(&iov_arr, &iovn);
+-                              i_assert(max_bytes >= 2);
+-                              max_bytes -= 2;
+-                              added++;
++                              i_assert(max_bytes >= iovn.iov_len);
++                              max_bytes -= iovn.iov_len;
++                              added += iovn.iov_len - 1;
+                               sent++;
+                       }
+               }
diff --git a/patches/0003-lib-sieve-Rename-on-disk-header-rusage-fields-to-unu.patch b/patches/0003-lib-sieve-Rename-on-disk-header-rusage-fields-to-unu.patch
new file mode 100644 (file)
index 0000000..cde5a75
--- /dev/null
@@ -0,0 +1,71 @@
+From e0c8bcd5d0d8bbd39edb2bc78cd15f469834660a Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Wed, 3 Jun 2026 12:10:46 +0000
+Subject: [PATCH 3/7] lib-sieve: Rename on-disk header rusage fields to
+ unused_*
+
+The .svbin header's resource_usage fields no longer carry data: they
+are zeroed on save and discarded on load. Cumulative CPU tracking
+lives in the per-user sieve-rusage file, and the in-memory carry-over
+lives in sbin->persisted_rusage.
+
+Flatten the nested resource_usage struct and rename the fields to
+unused_update_time and unused_cpu_time_msecs so it is obvious the
+fields are kept only for on-disk format compatibility. The memory
+layout of struct sieve_binary_header is unchanged.
+---
+ src/lib-sieve/sieve-binary-file.c    | 8 +++++---
+ src/lib-sieve/sieve-binary-private.h | 9 +++++----
+ 2 files changed, 10 insertions(+), 7 deletions(-)
+
+Index: trixie/pigeonhole/src/lib-sieve/sieve-binary-file.c
+===================================================================
+--- trixie.orig/pigeonhole/src/lib-sieve/sieve-binary-file.c
++++ trixie/pigeonhole/src/lib-sieve/sieve-binary-file.c
+@@ -147,13 +147,14 @@ sieve_binary_file_read_header(struct sie
+       return 0;
+ }
+ 
+-/* The on-disk binary header carries an unused resource_usage struct for
++/* The on-disk binary header carries unused resource_usage fields for
+    backward compatibility. Tracking lives in the per-user sieve-rusage file. */
+ static void sieve_binary_file_zero_header_rusage(struct sieve_binary *sbin)
+ {
+       struct sieve_binary_header *header = &sbin->header;
+ 
+-      i_zero(&header->resource_usage);
++      header->unused_update_time = 0;
++      header->unused_cpu_time_msecs = 0;
+       sieve_resource_usage_init(&sbin->rusage);
+       sbin->rusage_updated = FALSE;
+ }
+@@ -834,7 +835,8 @@ _sieve_binary_open(struct sieve_binary *
+ 
+       /* Resource usage tracking is no longer kept in the binary file;
+          discard any data from older versions. */
+-      i_zero(&sbin->header.resource_usage);
++      sbin->header.unused_update_time = 0;
++      sbin->header.unused_cpu_time_msecs = 0;
+       sbin->header.flags &= ENUM_NEGATE(SIEVE_BINARY_FLAG_RESOURCE_LIMIT);
+ 
+       /* Load block index */
+Index: trixie/pigeonhole/src/lib-sieve/sieve-binary-private.h
+===================================================================
+--- trixie.orig/pigeonhole/src/lib-sieve/sieve-binary-private.h
++++ trixie/pigeonhole/src/lib-sieve/sieve-binary-private.h
+@@ -26,10 +26,11 @@ struct sieve_binary_header {
+       uint32_t hdr_size;
+       uint32_t flags;
+ 
+-      struct {
+-              uint64_t update_time;
+-              uint32_t cpu_time_msecs;
+-      } resource_usage;
++      /* Resource usage tracking moved to per-user sieve-rusage file. These
++         fields are kept zeroed and present only for on-disk format
++         compatibility with older .svbin files. */
++      uint64_t unused_update_time;
++      uint32_t unused_cpu_time_msecs;
+ };
+ 
+ struct sieve_binary_file {
diff --git a/patches/0003-lib-sieve-edit_mail_headers_parse-Fix-info-leak-via-.patch b/patches/0003-lib-sieve-edit_mail_headers_parse-Fix-info-leak-via-.patch
new file mode 100644 (file)
index 0000000..e083d33
--- /dev/null
@@ -0,0 +1,271 @@
+From 76957df3b7ec3ca682e779fa91e22f6d8bb22740 Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Fri, 1 May 2026 06:28:33 +0000
+Subject: [PATCH 3/3] lib-sieve: edit_mail_headers_parse() - Fix info leak via
+ NUL-truncated header copy
+
+i_strndup() stops at the first NUL byte, so embedded NULs caused field->data
+to be shorter than field->size. This left stale heap data readable past the
+copied content when callers treat data+body_offset as a string.
+
+Adds a test-edit-mail unit test and a sieve testsuite case that both
+exercise edit_mail_headers_parse() with a NUL byte embedded in a header
+value. The unit test memcmp-verifies that the bytes after the NUL are
+preserved; the sieve test triggers an invalid heap read detectable by
+valgrind under the old code.
+---
+ Makefile.am                                  |   1 +
+ src/lib-sieve/util/edit-mail.c               |   8 +-
+ src/lib-sieve/util/test-edit-mail.c          | 140 +++++++++++++++++++
+ tests/extensions/editheader/nul-value.svtest |  46 ++++++
+ 4 files changed, 192 insertions(+), 3 deletions(-)
+ create mode 100644 tests/extensions/editheader/nul-value.svtest
+
+Index: trixie/pigeonhole/Makefile.am
+===================================================================
+--- trixie.orig/pigeonhole/Makefile.am
++++ trixie/pigeonhole/Makefile.am
+@@ -169,6 +169,7 @@ test_cases = \
+       tests/extensions/editheader/utf8.svtest \
+       tests/extensions/editheader/protected.svtest \
+       tests/extensions/editheader/errors.svtest \
++      tests/extensions/editheader/nul-value.svtest \
+       tests/extensions/editheader/execute.svtest \
+       tests/extensions/duplicate/errors.svtest \
+       tests/extensions/duplicate/execute.svtest \
+Index: trixie/pigeonhole/src/lib-sieve/util/edit-mail.c
+===================================================================
+--- trixie.orig/pigeonhole/src/lib-sieve/util/edit-mail.c
++++ trixie/pigeonhole/src/lib-sieve/util/edit-mail.c
+@@ -553,8 +553,9 @@ edit_mail_header_field_create(struct edi
+                                             &field->body_offset);
+ 
+               /* Copy to new field */
+-              field->data = i_strndup(str_data(data), str_len(data));
+               field->size = str_len(data);
++              field->data = i_malloc(field->size + 1);
++              memcpy(field->data, str_data(data), field->size);
+               field->virtual_size = (edmail->crlf ?
+                                      field->size : field->size + lines);
+               field->lines = lines;
+@@ -831,8 +832,9 @@ static int edit_mail_headers_parse(struc
+ 
+                       field->size = str_len(hdr_data);
+                       field->virtual_size = field->size + vsize_diff;
+-                      field->data = i_strndup(str_data(hdr_data),
+-                                              field->size);
++                      field->data = i_malloc(field->size + 1);
++                      memcpy(field->data, str_data(hdr_data),
++                             field->size);
+                       field->offset = offset;
+                       field->lines = lines;
+ 
+Index: trixie/pigeonhole/src/lib-sieve/util/test-edit-mail.c
+===================================================================
+--- trixie.orig/pigeonhole/src/lib-sieve/util/test-edit-mail.c
++++ trixie/pigeonhole/src/lib-sieve/util/test-edit-mail.c
+@@ -866,6 +866,144 @@ static void test_edit_mail_empty(void)
+       test_end();
+ }
+ 
++static void test_edit_mail_nul_in_header(void)
++{
++      /* Message with a NUL byte embedded in the X-Has-NUL header value.
++       * sizeof() - 1 gives the true byte count, skipping the C string's
++       * trailing NUL terminator, while preserving the embedded \x00. */
++      static const unsigned char message[] =
++              "From: sender@example.com\n"
++              "X-Has-NUL: value\x00" "afterNUL\n"
++              "Subject: Test\n"
++              "\n"
++              "Body\n";
++      /* Expected output after deleting the Subject header. */
++      static const unsigned char expected[] =
++              "From: sender@example.com\n"
++              "X-Has-NUL: value\x00" "afterNUL\n"
++              "\n"
++              "Body\n";
++      struct istream *input_msg, *input_mail;
++      buffer_t *buffer;
++      struct mail_raw *rawmail;
++      struct edit_mail *edmail;
++      struct mail *mail;
++
++      test_begin("edit-mail - NUL byte in header value");
++      test_edit_mail_init();
++
++      /* sizeof - 1 excludes the trailing C-string NUL terminator */
++      input_msg = i_stream_create_from_data(message, sizeof(message) - 1);
++
++      rawmail = mail_raw_open_stream(test_raw_mail_user, input_msg);
++      edmail = edit_mail_wrap(rawmail->mail);
++
++      /* Deleting any header triggers edit_mail_headers_parse(), which with
++       * the old i_strndup() allocated only strlen("X-Has-NUL: value")+1=17
++       * bytes for X-Has-NUL's field->data even though field->size=26.
++       * Streaming then called memcpy(dst, field->data, 26), reading 9 bytes
++       * past the end of the allocation and producing garbage in place of
++       * "afterNUL\n". */
++      edit_mail_header_delete(edmail, "Subject", 0);
++
++      mail = edit_mail_get_mail(edmail);
++
++      if (mail_get_stream(mail, NULL, NULL, &input_mail) < 0) {
++              i_fatal("Failed to open mail stream: %s",
++                      mailbox_get_last_internal_error(mail->box, NULL));
++      }
++
++      buffer = buffer_create_dynamic(default_pool, 128);
++
++      /* normal */
++
++      i_stream_seek(input_mail, 0);
++      test_stream_data(input_mail, buffer);
++
++      test_out("nul in header",
++               buffer->used == sizeof(expected) - 1 &&
++               memcmp(buffer->data, expected, sizeof(expected) - 1) == 0);
++
++      /* slow (byte-by-byte) */
++
++      i_stream_seek(input_mail, 0);
++      buffer_set_used_size(buffer, 0);
++      test_stream_data_slow(input_mail, buffer);
++
++      test_out("nul in header, slow",
++               buffer->used == sizeof(expected) - 1 &&
++               memcmp(buffer->data, expected, sizeof(expected) - 1) == 0);
++
++      /* clean up */
++
++      buffer_free(&buffer);
++      edit_mail_unwrap(&edmail);
++      mail_raw_close(&rawmail);
++      i_stream_unref(&input_msg);
++      test_edit_mail_deinit();
++      test_end();
++}
++
++static void test_edit_mail_empty2(void)
++{
++      struct istream *input_msg, *input_mail;
++      buffer_t *buffer;
++      struct mail_raw *rawmail;
++      struct edit_mail *edmail;
++      struct mail *mail;
++      const char *value;
++
++      test_begin("edit-mail - empty message (delete, add)");
++      test_edit_mail_init();
++
++      /* Compose the message */
++
++      input_msg = i_stream_create_from_data("", 0);
++
++      rawmail = mail_raw_open_stream(test_raw_mail_user, input_msg);
++
++      edmail = edit_mail_wrap(rawmail->mail);
++
++      /* Delete header */
++
++      edit_mail_header_delete(edmail, "X-B", 0);
++
++      /* Add header */
++
++      edit_mail_header_add(edmail, "X-B", "Frop", TRUE);
++      mail = edit_mail_get_mail(edmail);
++
++      /* Prepare tests */
++
++      if (mail_get_stream(mail, NULL, NULL, &input_mail) < 0) {
++              i_fatal("Failed to open mail stream: %s",
++                      mailbox_get_last_error(mail->box, NULL));
++      }
++
++      buffer = buffer_create_dynamic(default_pool, 1024);
++
++      /* Evaluate modified header */
++
++      test_assert(mail_get_first_header_utf8(mail, "X-B", &value) > 0 &&
++                  strcmp(value, "Frop") == 0);
++
++      /* Added */
++
++      i_stream_seek(input_mail, 0);
++      buffer_set_used_size(buffer, 0);
++
++      test_stream_data(input_mail, buffer);
++
++      /* Clean up */
++
++      buffer_free(&buffer);
++      edit_mail_unwrap(&edmail);
++      mail_raw_close(&rawmail);
++      i_stream_unref(&input_msg);
++      test_edit_mail_deinit();
++      test_end();
++}
++
+ int main(int argc, char *argv[])
+ {
+       static void (*test_functions[])(void) = {
+@@ -873,6 +1011,8 @@ int main(int argc, char *argv[])
+               test_edit_mail_big_header,
+               test_edit_mail_small_buffer,
+               test_edit_mail_empty,
++              test_edit_mail_empty2,
++              test_edit_mail_nul_in_header,
+               NULL
+       };
+       const enum master_service_flags service_flags =
+Index: trixie/pigeonhole/tests/extensions/editheader/nul-value.svtest
+===================================================================
+--- /dev/null
++++ trixie/pigeonhole/tests/extensions/editheader/nul-value.svtest
+@@ -0,0 +1,46 @@
++require "vnd.dovecot.testsuite";
++require "encoded-character";
++require "variables";
++require "fileinto";
++require "mailbox";
++
++require "editheader";
++
++/*
++ * Regression test: edit_mail_headers_parse() used i_strndup() to copy the
++ * raw header data, which stopped at the first NUL byte and allocated a buffer
++ * shorter than field->size.  When the modified message was subsequently
++ * streamed (e.g. fileinto), memcpy() read field->size bytes out of that
++ * under-sized buffer, causing an invalid heap read detectable by valgrind.
++ */
++
++/* Construct a raw message that has a NUL byte inside a header value.
++ * ${hex:00} = NUL.  field->size covers the full header line including
++ * bytes after the NUL; the old i_strndup() only allocated up to the NUL. */
++test_set "message" text:
++From: sender@example.com
++X-Has-NUL: value${hex:00}afterNUL
++Subject: Test
++
++Body
++.
++;
++
++test "editheader - NUL byte in header value" {
++      /* deleteheader calls edit_mail_headers_parse(), which allocates
++       * field->data for every header including X-Has-NUL.  With the old
++       * i_strndup() code, X-Has-NUL's allocation is only strlen("value")+1
++       * bytes, but field->size covers the full extent including bytes after
++       * the NUL.  deleteheader also sets modified=TRUE. */
++      deleteheader "Subject";
++
++      fileinto :create "nul-test";
++
++      /* Streaming iterates over all remaining parsed headers including
++       * X-Has-NUL, reading field->size bytes from field->data.  With the
++       * old under-sized allocation this is an invalid heap read that
++       * valgrind detects. */
++      if not test_result_execute {
++              test_fail "failed to execute result";
++      }
++}
diff --git a/patches/0003-lib-smtp-smtp-server-Expose-smtp_server_connection_r.patch b/patches/0003-lib-smtp-smtp-server-Expose-smtp_server_connection_r.patch
new file mode 100644 (file)
index 0000000..00c677a
--- /dev/null
@@ -0,0 +1,51 @@
+From 12fcbfe65431ece3f49d39d553f22001b3b007b6 Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Thu, 7 May 2026 10:57:59 +0000
+Subject: [PATCH 3/5] lib-smtp: smtp-server - Expose
+ smtp_server_connection_reply_immediate
+
+The function already existed internally as a way to write a reply directly
+to the output stream, bypassing the per-command reply queue. Make it part
+of the public API so callers that need a reply on the wire before tearing
+the connection down (where queued replies would otherwise be aborted) can
+use it.
+---
+ src/lib-smtp/smtp-server-private.h |  3 ---
+ src/lib-smtp/smtp-server.h         | 10 ++++++++++
+ 2 files changed, 10 insertions(+), 3 deletions(-)
+
+Index: trixie/src/lib-smtp/smtp-server-private.h
+===================================================================
+--- trixie.orig/src/lib-smtp/smtp-server-private.h
++++ trixie/src/lib-smtp/smtp-server-private.h
+@@ -323,9 +323,6 @@ void smtp_server_connection_reply_lines(
+                                       unsigned int status,
+                                       const char *enh_code,
+                                       const char *const *text_lines);
+-void smtp_server_connection_reply_immediate(
+-      struct smtp_server_connection *conn, unsigned int status,
+-      const char *fmt, ...) ATTR_FORMAT(3, 4);
+ 
+ void smtp_server_connection_reset_state(struct smtp_server_connection *conn);
+ void smtp_server_connection_set_state(struct smtp_server_connection *conn,
+Index: trixie/src/lib-smtp/smtp-server.h
+===================================================================
+--- trixie.orig/src/lib-smtp/smtp-server.h
++++ trixie/src/lib-smtp/smtp-server.h
+@@ -477,6 +477,16 @@ void smtp_server_connection_set_ssl_stre
+ void smtp_server_connection_close(struct smtp_server_connection **_conn,
+                                 const char *reason) ATTR_NULL(2);
+ bool smtp_server_connection_is_closed(struct smtp_server_connection *conn);
++/* Send a reply line directly to the output stream, bypassing the per-command
++   reply queue. Useful when the caller is about to close the connection and
++   needs the reply on the wire before close/destroy aborts queued replies.
++   The formatted text must be a single line (no CR/LF); unlike
++   smtp_server_reply_add_text(), this function does not split on \n nor add
++   the SMTP continuation prefix to subsequent lines. */
++void smtp_server_connection_reply_immediate(struct smtp_server_connection *conn,
++                                          unsigned int status,
++                                          const char *fmt, ...)
++                                          ATTR_FORMAT(3, 4);
+ void smtp_server_connection_terminate(struct smtp_server_connection **_conn,
+                                     const char *enh_code, const char *reason)
+                                     ATTR_NULL(3);
diff --git a/patches/0003-lib-sql-Add-sql_commit_schedule_delayed-helper.patch b/patches/0003-lib-sql-Add-sql_commit_schedule_delayed-helper.patch
new file mode 100644 (file)
index 0000000..e602549
--- /dev/null
@@ -0,0 +1,50 @@
+From 3c388ec11db1d61a2795241321d80a7dc245105c Mon Sep 17 00:00:00 2001
+From: Aki Tuomi <aki.tuomi@open-xchange.com>
+Date: Tue, 2 Jun 2026 09:54:41 +0000
+Subject: [PATCH 3/6] lib-sql: Add sql_commit_schedule_delayed() helper
+
+---
+ src/lib-sql/sql-api.c | 21 ++++++++++++++-------
+ 1 file changed, 14 insertions(+), 7 deletions(-)
+
+Index: trixie/src/lib-sql/sql-api.c
+===================================================================
+--- trixie.orig/src/lib-sql/sql-api.c
++++ trixie/src/lib-sql/sql-api.c
+@@ -885,6 +885,19 @@ static void sql_commit_delayed_callback(
+       i_free(cb);
+ }
+ 
++static void
++sql_commit_schedule_delayed(struct sql_db *db, const char *error,
++                          sql_commit_callback_t *callback, void *context)
++{
++      struct sql_commit_result_delayed *cb = i_new(struct sql_commit_result_delayed, 1);
++      cb->db = db;
++      cb->error = i_strdup(error);
++      cb->callback = callback;
++      cb->context = context;
++      cb->to = timeout_add_short(0, sql_commit_delayed_callback, cb);
++      DLLIST_PREPEND(&db->commit_delayed_list, cb);
++}
++
+ #undef sql_transaction_commit
+ void sql_transaction_commit(struct sql_transaction_context **_ctx,
+                           sql_commit_callback_t *callback, void *context)
+@@ -898,15 +911,9 @@ void sql_transaction_commit(struct sql_t
+               return;
+       }
+ 
+-      struct sql_commit_result_delayed *cb = i_new(struct sql_commit_result_delayed, 1);
+       const char *error = NULL;
+       ctx->db->v.transaction_commit_s(ctx, &error);
+-      cb->db = db;
+-      cb->error = i_strdup(error);
+-      cb->callback = callback;
+-      cb->context = context;
+-      cb->to = timeout_add_short(0, sql_commit_delayed_callback, cb);
+-      DLLIST_PREPEND(&db->commit_delayed_list, cb);
++      sql_commit_schedule_delayed(db, error, callback, context);
+ }
+ 
+ int sql_transaction_commit_s(struct sql_transaction_context **_ctx,
diff --git a/patches/0003-lib-storage-thread-Limit-ancestor-chain-traversal-de.patch b/patches/0003-lib-storage-thread-Limit-ancestor-chain-traversal-de.patch
new file mode 100644 (file)
index 0000000..4771e5e
--- /dev/null
@@ -0,0 +1,92 @@
+From e6934c6d1afdcecd63591d7f514ccb39e11d6ada Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Fri, 17 Apr 2026 13:15:54 +0000
+Subject: [PATCH 3/3] lib-storage: thread - Limit ancestor chain traversal
+ depth to prevent O(N^2) CPU usage
+
+The per-message References limit (MAIL_THREAD_REFERENCES_MAX) prevents a
+single crafted message from causing O(N^2) traversals in
+thread_node_has_ancestor(). However, multiple crafted messages each
+containing 1000 References entries can build an arbitrarily deep ancestor
+chain across the mailbox, causing the same quadratic blowup spread over
+many messages: processing email k costs O(k * MAIL_THREAD_REFERENCES_MAX)
+steps, giving O(M^2 * MAIL_THREAD_REFERENCES_MAX) total for M emails.
+
+Fix this by limiting the traversal depth in thread_node_has_ancestor() to
+MAIL_THREAD_REFERENCES_MAX steps. When the limit is reached the link is
+dropped, bounding per-email work to O(MAIL_THREAD_REFERENCES_MAX^2)
+regardless of how deep the chain was built by prior messages.
+---
+ src/lib-storage/index/index-thread-links.c | 31 +++++++++++++++++++---
+ 1 file changed, 28 insertions(+), 3 deletions(-)
+
+diff --git a/src/lib-storage/index/index-thread-links.c b/src/lib-storage/index/index-thread-links.c
+index 9affb8338d..0ce33a0772 100644
+--- a/src/lib-storage/index/index-thread-links.c
++++ b/src/lib-storage/index/index-thread-links.c
+@@ -31,12 +31,24 @@ static uint32_t thread_msg_add(struct mail_thread_cache *cache,
+ 
+ static bool thread_node_has_ancestor(struct mail_thread_cache *cache,
+                                    const struct mail_thread_node *node,
+-                                   const struct mail_thread_node *ancestor)
++                                   const struct mail_thread_node *ancestor,
++                                   bool *depth_exceeded_r)
+ {
++      unsigned int n = 0;
++
++      *depth_exceeded_r = FALSE;
+       while (node != ancestor) {
+               if (node->parent_idx == 0)
+                       return FALSE;
+-
++              if (++n > MAIL_THREAD_REFERENCES_MAX) {
++                      /* Ancestor chain is deeper than the per-message
++                         References limit. Multiple crafted messages can build
++                         an arbitrarily deep chain, causing O(N^2) traversals
++                         per email even with the per-message limit. Treat the
++                         link as unsafe to prevent the DoS. */
++                      *depth_exceeded_r = TRUE;
++                      return FALSE;
++              }
+               node = array_idx(&cache->thread_nodes, node->parent_idx);
+       }
+       return TRUE;
+@@ -47,6 +59,7 @@ static void thread_link_reference(struct mail_thread_cache *cache,
+ {
+       struct mail_thread_node *node, *parent, *child;
+       uint32_t idx;
++      bool depth_exceeded;
+ 
+       i_assert(parent_idx < cache->first_invalid_msgid_str_idx);
+ 
+@@ -62,7 +75,7 @@ static void thread_link_reference(struct mail_thread_cache *cache,
+       }
+ 
+       child->parent_link_refcount++;
+-      if (thread_node_has_ancestor(cache, parent, child)) {
++      if (thread_node_has_ancestor(cache, parent, child, &depth_exceeded)) {
+               if (parent == child) {
+                       /* loops to itself - ignore */
+                       return;
+@@ -88,6 +101,18 @@ static void thread_link_reference(struct mail_thread_cache *cache,
+                       node->child_unref_rebuilds = TRUE;
+               } while (node != child);
+               return;
++      } else if (depth_exceeded) {
++              /* Ancestor chain exceeded the depth limit; drop this link.
++                 parent_link_refcount was already incremented above and is
++                 intentionally kept that way: mail_thread_unref_link()
++                 walks References blindly at remove time and would hit
++                 i_assert(child->parent_link_refcount > 0) on the matching
++                 edge if we hadn't bumped it here. The trade-off is that
++                 the bumped refcount can leave child->parent_idx attached
++                 to a stale parent if another message later sets a real
++                 parent edge on this child and is then expunged - threading
++                 stays wrong until some other event triggers a rebuild. */
++              return;
+       } else if (child->parent_idx == parent_idx) {
+               /* The same link already exists */
+               return;
+-- 
+2.47.3
+
diff --git a/patches/0004-acl-dovecot-acl-list-is-never-supposed-to-be-a-symli.patch b/patches/0004-acl-dovecot-acl-list-is-never-supposed-to-be-a-symli.patch
new file mode 100644 (file)
index 0000000..382bfbd
--- /dev/null
@@ -0,0 +1,26 @@
+From a5d06e714ed25a6cb18fd3af99314e94fa080046 Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Wed, 3 Jun 2026 18:16:38 +0300
+Subject: [PATCH 4/5] acl: dovecot-acl-list is never supposed to be a symlink -
+ use O_NOFOLLOW flag
+
+---
+ src/plugins/acl/acl-backend-vfile-acllist.c | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+diff --git a/src/plugins/acl/acl-backend-vfile-acllist.c b/src/plugins/acl/acl-backend-vfile-acllist.c
+index 7dfcfdd393..d291b55594 100644
+--- a/src/plugins/acl/acl-backend-vfile-acllist.c
++++ b/src/plugins/acl/acl-backend-vfile-acllist.c
+@@ -115,7 +115,7 @@ static int acl_backend_vfile_acllist_read(struct acl_backend_vfile *backend)
+                       return 0;
+       }
+ 
+-      fd = open(path, O_RDONLY);
++      fd = open(path, O_RDONLY | O_NOFOLLOW);
+       if (fd == -1) {
+               if (errno == ENOENT) {
+                       backend->acllist_mtime = 0;
+-- 
+2.39.5
+
diff --git a/patches/0004-imap-login-Add-dict_reset-side-channel-command-to-im.patch b/patches/0004-imap-login-Add-dict_reset-side-channel-command-to-im.patch
new file mode 100644 (file)
index 0000000..f30699d
--- /dev/null
@@ -0,0 +1,55 @@
+From da4c52ac021b32bfc9f926f3c229c91ecb729cca Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Thu, 16 Apr 2026 17:38:45 +0200
+Subject: [PATCH 4/5] imap-login: Add dict_reset side-channel command to
+ imap-proxy
+
+When imap_compress_on_proxy is enabled, DEFLATE compression runs inside
+the imap-login process.  The imap backend process cannot call
+o_stream_deflate_reset_dict() directly on a remote stream, so this adds
+a new side-channel command "dict_reset" that the backend can send to
+trigger the dictionary reset on the proxy side.
+---
+ src/imap-login/imap-proxy.c | 14 ++++++++++++++
+ 1 file changed, 14 insertions(+)
+
+diff --git a/src/imap-login/imap-proxy.c b/src/imap-login/imap-proxy.c
+index 03dad690e1..2084cb290d 100644
+--- a/src/imap-login/imap-proxy.c
++++ b/src/imap-login/imap-proxy.c
+@@ -10,6 +10,7 @@
+ #include "str-sanitize.h"
+ #include "safe-memset.h"
+ #include "compression.h"
++#include "ostream-zlib.h"
+ #include "dsasl-client.h"
+ #include "imap-login-client.h"
+ #include "client-authenticate.h"
+@@ -641,11 +642,24 @@ proxy_side_cmd_compress(struct client *client, const char *const *args,
+       return 0;
+ }
+ 
++static int
++proxy_side_cmd_dict_reset(struct client *client,
++                        const char *const *args ATTR_UNUSED,
++                        const char **error_r ATTR_UNUSED)
++{
++      struct ostream *client_output =
++              login_proxy_get_client_ostream(client->login_proxy);
++      o_stream_deflate_reset_dict(client_output);
++      return 0;
++}
++
+ int imap_proxy_side_channel_input(struct client *client,
+                                 const char *const *args, const char **error_r)
+ {
+       if (strcmp(args[0], "compress") == 0)
+               return proxy_side_cmd_compress(client, args + 1, error_r);
++      else if (strcmp(args[0], "dict_reset") == 0)
++              return proxy_side_cmd_dict_reset(client, args + 1, error_r);
+       else {
+               *error_r = "Unsupported command";
+               return -1;
+-- 
+2.47.3
+
diff --git a/patches/0004-lib-Key-str_hash-strcase_hash-with-a-random-seed-to-.patch b/patches/0004-lib-Key-str_hash-strcase_hash-with-a-random-seed-to-.patch
new file mode 100644 (file)
index 0000000..059898c
--- /dev/null
@@ -0,0 +1,120 @@
+From b9afacaf42e7b2efff769dbc3eb10ac48934462a Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Mon, 13 Apr 2026 12:38:50 +0200
+Subject: [PATCH 04/12] lib: Key str_hash/strcase_hash with a random seed to
+ prevent HashDoS
+
+hash_init() fills a process-wide uint64_t hash_iv via random_fill()
+at lib_init() time (after random_init()). str_hash() and strcase_hash()
+pass hash_iv as the xxh64 seed so an attacker cannot predict bucket
+placement and manufacture collision chains.
+---
+ src/lib/hash.c | 12 ++++++++++--
+ src/lib/hash.h | 10 +++++++++-
+ src/lib/lib.c  |  2 ++
+ 3 files changed, 21 insertions(+), 3 deletions(-)
+
+diff --git a/src/lib/hash.c b/src/lib/hash.c
+index 61fe86ed8e..83eeed42f6 100644
+--- a/src/lib/hash.c
++++ b/src/lib/hash.c
+@@ -5,6 +5,7 @@
+ #include "lib.h"
+ #include "hash.h"
+ #include "primes.h"
++#include "randgen.h"
+ #include "xxh64.h"
+ 
+ #include <ctype.h>
+@@ -59,6 +60,8 @@ enum hash_table_operation{
+       HASH_TABLE_OP_RESIZE
+ };
+ 
++uint64_t hash_iv;
++
+ static bool hash_table_resize(struct hash_table *table, bool grow);
+ 
+ void hash_table_create(struct hash_table **table_r, pool_t node_pool,
+@@ -520,9 +523,14 @@ void hash_table_copy(struct hash_table *dest, struct hash_table *src)
+       hash_table_thaw(dest);
+ }
+ 
++void hash_init(void)
++{
++      random_fill(&hash_iv, sizeof(hash_iv));
++}
++
+ unsigned int str_hash(const char *p)
+ {
+-      return xxh64_to_32(xxh64_data(p, strlen(p), 0));
++      return xxh64_to_32(xxh64_data(p, strlen(p), hash_iv));
+ }
+ 
+ unsigned int str_stable_hash(const char *p)
+@@ -535,7 +543,7 @@ unsigned int strcase_hash(const char *p)
+       struct xxh64_context ctx;
+       unsigned char c;
+ 
+-      xxh64_init(&ctx, 0);
++      xxh64_init(&ctx, hash_iv);
+       while (*p != '\0') {
+               c = (unsigned char)i_toupper(*p++);
+               xxh64_loop(&ctx, &c, 1);
+diff --git a/src/lib/hash.h b/src/lib/hash.h
+index 84d9c52aef..534c25f6ee 100644
+--- a/src/lib/hash.h
++++ b/src/lib/hash.h
+@@ -14,6 +14,9 @@ typedef unsigned int hash_callback_t(const void *p);
+ /* Returns 0 if the pointers are equal. */
+ typedef int hash_cmp_callback_t(const void *p1, const void *p2);
+ 
++/* Random per-process IV to use for hash functions */
++extern uint64_t hash_iv;
++
+ /* Create a new hash table. If initial_size is 0, the default value is used.
+    table_pool is used to allocate/free large hash tables, node_pool is used
+    for smaller allocations and can also be alloconly pool. The pools must not
+@@ -168,9 +171,12 @@ void hash_table_copy(struct hash_table *dest, struct hash_table *src);
+ #define hash_table_copy(table1, table2) \
+       hash_table_copy((table1)._table, (table2)._table)
+ 
+-/* hash function for strings */
++/* Hash function for strings. These are safe against collision attacks. They
++   are initialized with a per-process random key. */
+ unsigned int str_hash(const char *p) ATTR_PURE;
+ unsigned int strcase_hash(const char *p) ATTR_PURE;
++/* Like str_hash(), but there is no per-process random key. This isn't as safe
++   against collision attacks. */
+ unsigned int str_stable_hash(const char *p) ATTR_PURE;
+ 
+ /* fast hash function which uppercases a-z. Does not work well
+@@ -181,4 +187,6 @@ unsigned int strfastcase_hash(const char *p) ATTR_PURE;
+ /* a generic hash for a given memory block */
+ unsigned int mem_hash(const void *p, unsigned int size) ATTR_PURE;
+ 
++void hash_init(void);
++
+ #endif
+diff --git a/src/lib/lib.c b/src/lib/lib.c
+index 4df939b7a1..4a4372f516 100644
+--- a/src/lib/lib.c
++++ b/src/lib/lib.c
+@@ -5,6 +5,7 @@
+ #include "array.h"
+ #include "event-filter.h"
+ #include "env-util.h"
++#include "hash.h"
+ #include "hostpid.h"
+ #include "ipwd.h"
+ #include "process-title.h"
+@@ -184,6 +185,7 @@ void lib_init(void)
+ {
+       i_assert(!lib_initialized);
+       random_init();
++      hash_init();
+       data_stack_init();
+       hostpid_init();
+       lib_open_non_stdio_dev_null();
+-- 
+2.47.3
+
diff --git a/patches/0004-lib-compression-test-Add-zero-len-frame-test-for-all.patch b/patches/0004-lib-compression-test-Add-zero-len-frame-test-for-all.patch
new file mode 100644 (file)
index 0000000..e432e08
--- /dev/null
@@ -0,0 +1,72 @@
+From 32ef85eff6329395b74e47ec69bf7926c629d7c6 Mon Sep 17 00:00:00 2001
+From: Aki Tuomi <aki.tuomi@open-xchange.com>
+Date: Fri, 12 Jun 2026 11:19:42 +0000
+Subject: [PATCH 4/4] lib-compression: test - Add zero-len frame test for all
+ handlers
+
+Verify that successive empty compressed frames produce a clean EOF
+across all compression algorithms. lz4 is limited to one frame as it
+uses a custom single-stream format without concatenation support.
+---
+ src/lib-compression/test-compression.c | 41 ++++++++++++++++++++++++++
+ 1 file changed, 41 insertions(+)
+
+Index: trixie/src/lib-compression/test-compression.c
+===================================================================
+--- trixie.orig/src/lib-compression/test-compression.c
++++ trixie/src/lib-compression/test-compression.c
+@@ -155,6 +155,46 @@ test_compression_handler_empty(const str
+ }
+ 
+ static void
++test_compression_handler_zero_frame(const struct compression_handler *handler,
++                                  bool autodetect)
++{
++      test_begin(t_strdup_printf("compression handler %s (zero-len frame, autodetect=%s)",
++                                 handler->name, autodetect ? "yes" : "no"));
++
++      /* Produce successive empty frames. Each frame needs its own ostream
++         because o_stream_finish() propagates to the parent; compress each
++         into a separate buffer then concatenate.
++         lz4 uses a custom single-stream format and does not support
++         concatenated frames, so limit it to one. */
++      unsigned int n_frames = strcmp(handler->name, "lz4") == 0 ? 1 : 3;
++      buffer_t *compressed = buffer_create_dynamic(pool_datastack_create(), 256);
++      for (unsigned int i = 0; i < n_frames; i++) {
++              buffer_t *frame_buf = buffer_create_dynamic(pool_datastack_create(), 64);
++              struct ostream *os = test_ostream_create(frame_buf);
++              struct ostream *output = handler->create_ostream_auto(os, set.event);
++              o_stream_unref(&os);
++              test_assert_idx(o_stream_finish(output) == 1, i);
++              o_stream_unref(&output);
++              test_assert_idx(frame_buf->used > 0, i);
++              buffer_append(compressed, frame_buf->data, frame_buf->used);
++      }
++
++      /* Decompress: must yield clean EOF with no error and no loop. */
++      struct istream *is = test_istream_create_data(compressed->data, compressed->used);
++      is->blocking = TRUE;
++      struct istream *input = !autodetect ? handler->create_istream(is) :
++              i_stream_create_decompress(is, 0);
++      i_stream_unref(&is);
++
++      test_assert(i_stream_read(input) == -1);
++      test_assert(input->eof);
++      test_assert(input->stream_errno == 0);
++      i_stream_unref(&input);
++
++      test_end();
++}
++
++static void
+ test_compression_handler_seek(const struct compression_handler *handler,
+                             bool autodetect)
+ {
+@@ -743,6 +783,7 @@ static void test_compression_int(bool au
+                               test_compression_handler_detect(&compression_handlers[i]);
+                       test_compression_handler_short(&compression_handlers[i], autodetect);
+                       test_compression_handler_empty(&compression_handlers[i], autodetect);
++                      test_compression_handler_zero_frame(&compression_handlers[i], autodetect);
+                       test_compression_handler(&compression_handlers[i], autodetect);
+                       test_compression_handler_seek(&compression_handlers[i], autodetect);
+                       test_compression_handler_reset(&compression_handlers[i], autodetect);
diff --git a/patches/0004-lib-mail-message-parser-from-parts-Enforce-50-MB-all.patch b/patches/0004-lib-mail-message-parser-from-parts-Enforce-50-MB-all.patch
new file mode 100644 (file)
index 0000000..31de9be
--- /dev/null
@@ -0,0 +1,52 @@
+From afb5fd924bf39dc790a573151e2d2626a63d0aec Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Thu, 16 Apr 2026 22:38:02 +0200
+Subject: [PATCH 04/14] lib-mail: message-parser-from-parts: Enforce 50 MB
+ all-headers-max-size limit
+
+The preparsed (from-parts) header parser was not tracking
+all_headers_total_size and never called message_parse_header_lower_limit(),
+so the cumulative 50 MB header size limit was never applied when re-parsing
+a message using cached part structure.
+
+Fix by mirroring the same tracking that message-parser.c does in its
+parse_next_header_block(): update all_headers_total_size for each parsed
+header line, and call message_parse_header_lower_limit() with the remaining
+budget when initialising the per-part header parser.
+
+Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
+---
+ src/lib-mail/message-parser-from-parts.c | 10 ++++++++++
+ 1 file changed, 10 insertions(+)
+
+diff --git a/src/lib-mail/message-parser-from-parts.c b/src/lib-mail/message-parser-from-parts.c
+index 867a916c1c..fb75dc7148 100644
+--- a/src/lib-mail/message-parser-from-parts.c
++++ b/src/lib-mail/message-parser-from-parts.c
+@@ -303,6 +303,11 @@ static int preparsed_parse_next_header(struct message_parser_ctx *ctx,
+       }
+ 
+       if (hdr != NULL) {
++              if (!hdr->continues) {
++                      ctx->all_headers_total_size += hdr->name_len;
++                      ctx->all_headers_total_size += hdr->middle_len;
++              }
++              ctx->all_headers_total_size += hdr->value_len;
+               block_r->hdr = hdr;
+               block_r->size = 0;
+               return 1;
+@@ -344,6 +349,11 @@ static int preparsed_parse_next_header_init(struct message_parser_ctx *ctx,
+               message_parse_header_init(hdr_input, NULL, ctx->hdr_flags);
+       i_stream_unref(&hdr_input);
+ 
++      size_t headers_available =
++              ctx->all_headers_max_size > ctx->all_headers_total_size ?
++              ctx->all_headers_max_size - ctx->all_headers_total_size : 0;
++      message_parse_header_lower_limit(ctx->hdr_parser_ctx, headers_available);
++
+       ctx->parse_next_block = preparsed_parse_next_header;
+       return preparsed_parse_next_header(ctx, block_r);
+ }
+-- 
+2.47.3
+
diff --git a/patches/0004-lib-mail-o_stream_dot_sendv-Do-not-send-unguarded-.-.patch b/patches/0004-lib-mail-o_stream_dot_sendv-Do-not-send-unguarded-.-.patch
new file mode 100644 (file)
index 0000000..84b2306
--- /dev/null
@@ -0,0 +1,216 @@
+From dc1f33030101d90fcb77612eb3a111854525506a Mon Sep 17 00:00:00 2001
+From: Marco Bettini <marco.bettini@open-xchange.com>
+Date: Wed, 8 Apr 2026 10:14:08 +0000
+Subject: [PATCH 4/4] lib-mail: o_stream_dot_sendv() - Do not send unguarded
+ '.' after bare '\r'
+
+---
+ src/lib-mail/ostream-dot.c       | 40 ++++++++++++++++--
+ src/lib-mail/test-iostream-dot.c |  9 ++++
+ src/lib-mail/test-ostream-dot.c  | 71 ++++++++++++++++++++++++++++++--
+ 3 files changed, 112 insertions(+), 8 deletions(-)
+
+Index: trixie/src/lib-mail/ostream-dot.c
+===================================================================
+--- trixie.orig/src/lib-mail/ostream-dot.c
++++ trixie/src/lib-mail/ostream-dot.c
+@@ -71,10 +71,12 @@ o_stream_dot_close(struct iostream_priva
+               o_stream_close(dstream->ostream.parent);
+ }
+ 
++#define ADD_MAX 3
+ enum o_stream_dot_sendv_add {
+       ADD_NONE,
+       ADD_CR,
+       ADD_DOT,
++      ADD_LF_DOT,
+ };
+ 
+ static ssize_t
+@@ -111,14 +113,14 @@ o_stream_dot_sendv(struct ostream_privat
+ 
+               p = data;
+               pend = CONST_PTR_OFFSET(data, size);
+-              for (; p < pend && ((size_t)(p - data) + 2) <= max_bytes; p++) {
++              for (; p < pend && ((size_t)(p - data) + ADD_MAX) <= max_bytes; p++) {
+                       enum o_stream_dot_sendv_add add = ADD_NONE;
+ 
+                       size = pend - p;
+                       switch (dstream->state) {
+                       /* none */
+                       case STREAM_STATE_NONE: {
+-                              size_t maxlen = I_MIN(size, max_bytes - ((size_t)(p - data) + 2));
++                              size_t maxlen = I_MIN(size, max_bytes - ((size_t)(p - data) + ADD_MAX));
+                               p = CONST_PTR_OFFSET(p, i_memcspn(p, maxlen, "\r\n", 2));
+                               i_assert(p <= pend);
+                               if (p == pend) {
+@@ -145,6 +147,9 @@ o_stream_dot_sendv(struct ostream_privat
+                               case '\n':
+                                       dstream->state = STREAM_STATE_CRLF;
+                                       break;
++                              case '.':
++                                      add = ADD_LF_DOT;
++                                      /* fall through */
+                               default:
+                                       dstream->state = STREAM_STATE_NONE;
+                                       break;
+@@ -186,9 +191,31 @@ o_stream_dot_sendv(struct ostream_privat
+                                       max_bytes -= chunk;
+                                       sent += chunk;
+                               }
+-                              /* insert byte (substitute one with pair) */
+                               data++;
+ 
++                              /* Apply the modifications required according to RFC 5321 to the
++                                 stream :
++
++                                 ADD_DOT    - Apply standard SMTP dot-stuffing
++
++                                 ADD_CR     - Convert invalid lone LFs to CRLF to comply with
++                                              DATA line termination requirements, and avoid
++                                              potentially emitting bare "\n." sequences (same
++                                              threat as below for "\r." sequences).
++
++                                 ADD_LF_DOT - Guard against invalid "\r." sequences.
++                                              A downstream MTA that incorrectly handles bare CRs
++                                              as line terminators could interpret a '.' at the
++                                              start of the next line as end-of-DATA. This would
++                                              allow any following bytes to be processed as SMTP
++                                              commands.
++
++                                 ADD_CR and ADD_LF_DOT make the istream-dot round-trip lossy,
++                                 as these introduce bytes not present in the original input.
++                                 We accept this tradeoff, since preserving exact byte fidelity
++                                 for malformed input is less important than eliminating an
++                                 injection vector. */
++
+                               switch(add) {
+                               case ADD_DOT:
+                                       iovn.iov_base = "..";
+@@ -198,12 +225,17 @@ o_stream_dot_sendv(struct ostream_privat
+                                       iovn.iov_base = "\r\n";
+                                       iovn.iov_len = 2;
+                                       break;
++                              case ADD_LF_DOT:
++                                      iovn.iov_base = "\n..";
++                                      iovn.iov_len = 3;
++                                      break;
+                               default:
+                                       i_unreached();
+                               }
+ 
+                               array_push_back(&iov_arr, &iovn);
+-                              i_assert(max_bytes >= iovn.iov_len);
++                              i_assert(iovn.iov_len <= ADD_MAX);
++                              i_assert(iovn.iov_len <= max_bytes);
+                               max_bytes -= iovn.iov_len;
+                               added += iovn.iov_len - 1;
+                               sent++;
+Index: trixie/src/lib-mail/test-ostream-dot.c
+===================================================================
+--- trixie.orig/src/lib-mail/test-ostream-dot.c
++++ trixie/src/lib-mail/test-ostream-dot.c
+@@ -57,6 +57,9 @@ static void test_ostream_dot(void)
+               { "foo\n.\n", "foo\r\n..\r\n.\r\n" },
+               { ".foo\r\n.\r\nfoo\r\n", "..foo\r\n..\r\nfoo\r\n.\r\n" },
+               { ".foo\n.\nfoo\n", "..foo\r\n..\r\nfoo\r\n.\r\n" },
++              { ".", "..\r\n.\r\n" },
++              { "\r.", "\r\n..\r\n.\r\n" },
++              { "\r\r.", "\r\r\n..\r\n.\r\n" },
+               { "\r\n", "\r\n.\r\n" },
+               { "\n", "\r\n.\r\n" },
+               { "", "\r\n.\r\n" },
+@@ -92,7 +95,7 @@ static void test_ostream_dot_parent_almo
+       test_end();
+ }
+ 
+-static void test_ostream_dot_parent_exact_fit(void)
++static void test_ostream_dot_parent_max_bytes_boundary(void)
+ {
+       buffer_t *output_data;
+       struct ostream *test_output, *output;
+@@ -100,8 +103,8 @@ static void test_ostream_dot_parent_exac
+ 
+       test_begin("dot ostream parent exact fit");
+       output_data = t_buffer_create(1024);
+-      test_output = test_ostream_create_nonblocking(output_data, 2);
+-      test_ostream_set_max_output_size(test_output, 2);
++      test_output = test_ostream_create_nonblocking(output_data, 3);
++      test_ostream_set_max_output_size(test_output, 3);
+ 
+       output = o_stream_create_dot(test_output, FALSE);
+       ret = o_stream_send(output, ".", 1);
+@@ -114,12 +117,72 @@ static void test_ostream_dot_parent_exac
+       test_end();
+ }
+ 
++/* STATE_CR must persist across sendv calls so that a bare CR ending one send
++   followed by '.' starting the next still triggers ADD_LF_DOT.
++   A stateless per-call implementation would emit the '.' unguarded and let a
++   downstream MTA that treats bare CR as EOL interpret it as end-of-DATA. */
++static void test_ostream_dot_cr_across_sendv(void)
++{
++      buffer_t *output_data;
++      struct ostream *test_output, *output;
++      ssize_t ret;
++      const char *expected = "\r\n..\r\n.\r\n";
++
++      test_begin("dot ostream CR across sendv calls");
++      output_data = t_buffer_create(1024);
++      test_output = o_stream_create_buffer(output_data);
++
++      output = o_stream_create_dot(test_output, FALSE);
++      ret = o_stream_send(output, "\r", 1);
++      test_assert(ret == 1);
++      ret = o_stream_send(output, ".", 1);
++      test_assert(ret == 1);
++      test_assert(o_stream_finish(output) > 0);
++
++      o_stream_unref(&output);
++      o_stream_unref(&test_output);
++
++      test_assert_ucmp(str_len(output_data), ==, strlen(expected));
++      test_assert_memcmp(str_c(output_data), str_len(output_data),
++      expected, strlen(expected));
++      test_end();
++}
++
++/* Off-by-one regression guard for the bare-CR + '.' injection.
++   Input "\r." places '.' at offset 1; the ADD_LF_DOT pair injects 3 bytes
++   ("\n.."). The loop guard check becomes (1 + 3) <= max_bytes, so max_bytes==4
++   is the exact-fit case. */
++static void test_ostream_dot_cr_dot_exact_fit(void)
++{
++      buffer_t *output_data;
++      struct ostream *test_output, *output;
++      ssize_t ret;
++
++      test_begin("dot ostream CR+dot exact fit");
++      output_data = t_buffer_create(1024);
++      test_output = test_ostream_create_nonblocking(output_data, 4);
++      test_ostream_set_max_output_size(test_output, 4);
++
++      output = o_stream_create_dot(test_output, FALSE);
++      ret = o_stream_send(output, "\r.", 2);
++      test_assert(ret == 2);
++      test_assert_ucmp(output_data->used, ==, 4);
++      test_assert_memcmp(output_data->data, output_data->used,
++      "\r\n..", 4);
++
++      o_stream_unref(&output);
++      o_stream_unref(&test_output);
++      test_end();
++}
++
+ int main(void)
+ {
+       static void (*const test_functions[])(void) = {
+               test_ostream_dot,
+               test_ostream_dot_parent_almost_full,
+-              test_ostream_dot_parent_exact_fit,
++              test_ostream_dot_parent_max_bytes_boundary,
++              test_ostream_dot_cr_across_sendv,
++              test_ostream_dot_cr_dot_exact_fit,
+               NULL
+       };
+       return test_run(test_functions);
diff --git a/patches/0004-lib-sieve-Switch-sieve_max_cpu_time-enforcement-to-t.patch b/patches/0004-lib-sieve-Switch-sieve_max_cpu_time-enforcement-to-t.patch
new file mode 100644 (file)
index 0000000..c6c7f99
--- /dev/null
@@ -0,0 +1,294 @@
+From 7a55953924c54dc116150ae809b976456977db75 Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Tue, 5 May 2026 16:30:23 +0000
+Subject: [PATCH 4/7] lib-sieve: Switch sieve_max_cpu_time enforcement to
+ time-decayed re-enable
+
+When a user's cumulative CPU time exceeded sieve_max_cpu_time, the
+script was permanently disabled. SIEVE_BINARY_FLAG_RESOURCE_LIMIT was
+set in the binary header on first overrun and was never cleared by
+the resource_usage_timeout decay (which only zeroed the cpu_time
+field), so sieve_binary_check_executable() refused execution forever
+unless the binary was recompiled or an admin manually wiped the file.
+
+Replace the sticky-flag model with one driven purely by the persisted
+cumulative cpu_time, decayed by resource_usage_timeout:
+
+ - Drop SIEVE_BINARY_FLAG_RESOURCE_LIMIT and the enum that defined it;
+   nothing reads sbin->header.flags anymore.
+ - sieve_binary_check_executable() and sieve_binary_check_resource_usage()
+   call sieve_resource_usage_is_excessive() on the current cumulative
+   cpu_time. Raising sieve_max_cpu_time now correctly re-enables a
+   previously over-limit user without further intervention.
+ - Gate per-user file persistence on the per-run delta, not the
+   cumulative total. A refused-without-running attempt has a zero
+   delta, skips the file write, and leaves update_time at the moment
+   work was actually consumed. After resource_usage_timeout seconds
+   of inactivity (or only-trivial deliveries) the next load decays
+   the cpu_time to zero and the user is allowed to run again.
+
+The on-disk per-user sieve-rusage file format becomes:
+
+    V1 <cpu-secs> <update-time>
+---
+ src/lib-sieve/sieve-binary-file.c    | 28 ++++++++++++++--------------
+ src/lib-sieve/sieve-binary-private.h |  4 ----
+ src/lib-sieve/sieve-binary.c         | 13 ++-----------
+ src/lib-sieve/sieve-rusage.c         | 25 ++++++++-----------------
+ src/lib-sieve/sieve-rusage.h         | 14 +++++---------
+ 5 files changed, 29 insertions(+), 55 deletions(-)
+
+Index: trixie/pigeonhole/src/lib-sieve/sieve-binary-file.c
+===================================================================
+--- trixie.orig/pigeonhole/src/lib-sieve/sieve-binary-file.c
++++ trixie/pigeonhole/src/lib-sieve/sieve-binary-file.c
+@@ -315,7 +315,7 @@ sieve_binary_save_to_stream(struct sieve
+       header->blocks = blk_count;
+       header->hdr_size = sizeof(*header);
+ 
+-      header->flags &= ENUM_NEGATE(SIEVE_BINARY_FLAG_RESOURCE_LIMIT);
++      header->flags = 0;
+       sieve_binary_file_zero_header_rusage(sbin);
+ 
+       if (!_save_aligned(sbin, stream, header, sizeof(*header), NULL)) {
+@@ -837,7 +837,7 @@ _sieve_binary_open(struct sieve_binary *
+          discard any data from older versions. */
+       sbin->header.unused_update_time = 0;
+       sbin->header.unused_cpu_time_msecs = 0;
+-      sbin->header.flags &= ENUM_NEGATE(SIEVE_BINARY_FLAG_RESOURCE_LIMIT);
++      sbin->header.flags = 0;
+ 
+       /* Load block index */
+ 
+@@ -934,15 +934,16 @@ int sieve_binary_check_executable(struct
+                                 enum sieve_error *error_code_r,
+                                 const char **client_error_r)
+ {
++      struct sieve_resource_usage rusage;
++
+       *client_error_r = NULL;
+       sieve_error_args_init(&error_code_r, NULL);
+ 
+-      if (HAS_ALL_BITS(sbin->header.flags,
+-                       SIEVE_BINARY_FLAG_RESOURCE_LIMIT)) {
++      sieve_binary_get_resource_usage(sbin, &rusage);
++      if (sieve_resource_usage_is_excessive(sbin->svinst, &rusage)) {
+               e_debug(sbin->event,
+                       "Binary execution is blocked: "
+-                      "Cumulative resource usage limit exceeded "
+-                      "(resource limit flag is set)");
++                      "Cumulative resource usage limit exceeded");
+               *error_code_r = SIEVE_ERROR_RESOURCE_LIMIT;
+               *client_error_r = "cumulative resource usage limit exceeded";
+               return 0;
+@@ -959,8 +960,6 @@ int sieve_binary_file_update_resource_us
+ {
+       struct sieve_storage *storage;
+       struct sieve_resource_usage delta;
+-      struct sieve_resource_usage total;
+-      uint32_t flags;
+ 
+       sieve_error_args_init(&error_code_r, NULL);
+ 
+@@ -976,18 +975,19 @@ int sieve_binary_file_update_resource_us
+       storage = sbin->script->storage;
+ 
+       delta = sbin->rusage;
+-      sieve_binary_get_resource_usage(sbin, &total);
+-      flags = sbin->header.flags & SIEVE_BINARY_FLAG_RESOURCE_LIMIT;
+ 
+-      if (!HAS_ALL_BITS(flags, SIEVE_BINARY_FLAG_RESOURCE_LIMIT) &&
+-          !sieve_resource_usage_is_high(sbin->svinst, &total)) {
+-              /* Nothing meaningful to persist */
++      /* Persist only when this run consumed a meaningful amount of CPU.
++         Critically, refused-without-running attempts (delta == 0) must not
++         touch the file - otherwise the file's update_time would be refreshed
++         on every blocked attempt, preventing the resource_usage_timeout from
++         ever decaying the persisted CPU time and re-enabling the user. */
++      if (!sieve_resource_usage_is_high(sbin->svinst, &delta)) {
+               sieve_resource_usage_init(&sbin->rusage);
+               sbin->rusage_updated = FALSE;
+               return 0;
+       }
+ 
+-      if (sieve_rusage_storage_add(storage, &delta, flags) < 0) {
++      if (sieve_rusage_storage_add(storage, &delta) < 0) {
+               *error_code_r = SIEVE_ERROR_TEMP_FAILURE;
+               return -1;
+       }
+Index: trixie/pigeonhole/src/lib-sieve/sieve-binary-private.h
+===================================================================
+--- trixie.orig/pigeonhole/src/lib-sieve/sieve-binary-private.h
++++ trixie/pigeonhole/src/lib-sieve/sieve-binary-private.h
+@@ -13,10 +13,6 @@
+  * Binary file
+  */
+ 
+-enum SIEVE_BINARY_FLAGS {
+-      SIEVE_BINARY_FLAG_RESOURCE_LIMIT = BIT(0),
+-};
+-
+ struct sieve_binary_header {
+       uint32_t magic;
+       uint16_t version_major;
+Index: trixie/pigeonhole/src/lib-sieve/sieve-binary.c
+===================================================================
+--- trixie.orig/pigeonhole/src/lib-sieve/sieve-binary.c
++++ trixie/pigeonhole/src/lib-sieve/sieve-binary.c
+@@ -210,17 +210,14 @@ void sieve_binary_apply_persisted_rusage
+ {
+       struct sieve_storage *storage = sieve_binary_get_user_storage(sbin);
+       struct sieve_resource_usage rusage;
+-      struct sieve_binary_header *header = &sbin->header;
+-      uint32_t flags = 0;
+ 
+       if (storage == NULL)
+               return;
+-      if (sieve_rusage_storage_load(storage, &rusage, &flags) <= 0)
++      if (sieve_rusage_storage_load(storage, &rusage) <= 0)
+               return;
+ 
+       sbin->persisted_rusage.cpu_time_msecs = rusage.cpu_time_msecs;
+       sbin->persisted_rusage.update_time = ioloop_time;
+-      header->flags |= (flags & SIEVE_BINARY_FLAG_RESOURCE_LIMIT);
+       sieve_resource_usage_init(&sbin->rusage);
+       sbin->rusage_updated = FALSE;
+ }
+@@ -241,16 +238,10 @@ void sieve_binary_get_resource_usage(str
+ 
+ bool sieve_binary_check_resource_usage(struct sieve_binary *sbin)
+ {
+-      struct sieve_binary_header *header = &sbin->header;
+       struct sieve_resource_usage rusage;
+ 
+       sieve_binary_get_resource_usage(sbin, &rusage);
+-
+-      if (sieve_resource_usage_is_excessive(sbin->svinst, &rusage)) {
+-              header->flags |= SIEVE_BINARY_FLAG_RESOURCE_LIMIT;
+-              return FALSE;
+-      }
+-      return TRUE;
++      return !sieve_resource_usage_is_excessive(sbin->svinst, &rusage);
+ }
+ 
+ bool sieve_binary_record_resource_usage(
+Index: trixie/pigeonhole/src/lib-sieve/sieve-rusage.c
+===================================================================
+--- trixie.orig/pigeonhole/src/lib-sieve/sieve-rusage.c
++++ trixie/pigeonhole/src/lib-sieve/sieve-rusage.c
+@@ -26,12 +26,11 @@
+ #define SIEVE_RUSAGE_LOCK_TIMEOUT 10
+ #define SIEVE_RUSAGE_LOCK_STALE_TIMEOUT 60
+ 
+-/* Maximum size of the on-disk file. Single-line ASCII format with four
++/* Maximum size of the on-disk file. Single-line ASCII format with three
+    space-separated tokens; anything larger is treated as corrupt. */
+ #define SIEVE_RUSAGE_MAX_FILE_SIZE 128
+ 
+ struct sieve_rusage_record {
+-      uint32_t flags;
+       uint32_t cpu_time_msecs;
+       time_t update_time;
+ };
+@@ -72,13 +71,11 @@ sieve_rusage_parse(const char *line, str
+                   strcmp(tokens[0], SIEVE_RUSAGE_VERSION_TAG) != 0)
+                       break;
+               if (tokens[1] == NULL || tokens[2] == NULL ||
+-                  tokens[3] == NULL || tokens[4] != NULL)
++                  tokens[3] != NULL)
+                       break;
+-              if (str_to_uint32(tokens[1], &rec->flags) < 0)
++              if (str_to_uint32(tokens[1], &cpu_secs) < 0)
+                       break;
+-              if (str_to_uint32(tokens[2], &cpu_secs) < 0)
+-                      break;
+-              if (str_to_time(tokens[3], &rec->update_time) < 0)
++              if (str_to_time(tokens[2], &rec->update_time) < 0)
+                       break;
+               if (cpu_secs > UINT32_MAX / 1000)
+                       rec->cpu_time_msecs = UINT32_MAX;
+@@ -148,14 +145,12 @@ sieve_rusage_dotlock_settings(struct dot
+ }
+ 
+ int sieve_rusage_storage_load(struct sieve_storage *storage,
+-                            struct sieve_resource_usage *rusage_r,
+-                            uint32_t *flags_r)
++                            struct sieve_resource_usage *rusage_r)
+ {
+       struct sieve_rusage_record rec;
+       unsigned int timeout;
+ 
+       sieve_resource_usage_init(rusage_r);
+-      *flags_r = 0;
+ 
+       if (storage->rusage_path == NULL)
+               return 0;
+@@ -171,13 +166,11 @@ int sieve_rusage_storage_load(struct sie
+       }
+ 
+       rusage_r->cpu_time_msecs = rec.cpu_time_msecs;
+-      *flags_r = rec.flags;
+       return 1;
+ }
+ 
+ int sieve_rusage_storage_add(struct sieve_storage *storage,
+-                           const struct sieve_resource_usage *delta_rusage,
+-                           uint32_t flags_to_set)
++                           const struct sieve_resource_usage *delta_rusage)
+ {
+       struct sieve_rusage_record rec;
+       struct dotlock_settings dlset;
+@@ -226,7 +219,6 @@ int sieve_rusage_storage_add(struct siev
+           (ioloop_time - rec.update_time) > (time_t)timeout) {
+               /* decayed: discard previous values */
+               rec.cpu_time_msecs = 0;
+-              rec.flags = 0;
+       }
+ 
+       old_cpu = rec.cpu_time_msecs;
+@@ -234,7 +226,6 @@ int sieve_rusage_storage_add(struct siev
+               rec.cpu_time_msecs = UINT32_MAX;
+       else
+               rec.cpu_time_msecs = old_cpu + delta_rusage->cpu_time_msecs;
+-      rec.flags |= flags_to_set;
+       rec.update_time = ioloop_time;
+ 
+       /* Round CPU time up to whole seconds for persistence. */
+@@ -244,8 +235,8 @@ int sieve_rusage_storage_add(struct siev
+               cpu_secs = (rec.cpu_time_msecs + 999) / 1000;
+ 
+       out = t_str_new(64);
+-      str_printfa(out, "%s %u %u %lld\n", SIEVE_RUSAGE_VERSION_TAG,
+-                  rec.flags, cpu_secs, (long long)rec.update_time);
++      str_printfa(out, "%s %u %lld\n", SIEVE_RUSAGE_VERSION_TAG,
++                  cpu_secs, (long long)rec.update_time);
+ 
+       wret = write(fd, str_data(out), str_len(out));
+       if (wret != (ssize_t)str_len(out)) {
+Index: trixie/pigeonhole/src/lib-sieve/sieve-rusage.h
+===================================================================
+--- trixie.orig/pigeonhole/src/lib-sieve/sieve-rusage.h
++++ trixie/pigeonhole/src/lib-sieve/sieve-rusage.h
+@@ -16,17 +16,13 @@ void sieve_rusage_storage_init(struct si
+ 
+ /* Load persisted resource usage from the per-user file. Returns 1 on success,
+    0 if the storage has no rusage file (no INBOX namespace path), -1 on error.
+-   Applies the resource_usage_timeout decay. flags_r receives sieve binary
+-   header flags (RESOURCE_LIMIT) that were persisted. */
++   Applies the resource_usage_timeout decay. */
+ int sieve_rusage_storage_load(struct sieve_storage *storage,
+-                            struct sieve_resource_usage *rusage_r,
+-                            uint32_t *flags_r);
++                            struct sieve_resource_usage *rusage_r);
+ 
+-/* Atomically add delta_rusage to and OR flags_to_set into the persisted
+-   per-user rusage file. Read-modify-write under fcntl WRLCK. Returns 1 on
+-   success, 0 if no rusage file is configured, -1 on error. */
++/* Atomically add delta_rusage to the persisted per-user rusage file.
++   Returns 1 on success, 0 if no rusage file is configured, -1 on error. */
+ int sieve_rusage_storage_add(struct sieve_storage *storage,
+-                           const struct sieve_resource_usage *delta_rusage,
+-                           uint32_t flags_to_set);
++                           const struct sieve_resource_usage *delta_rusage);
+ 
+ #endif
diff --git a/patches/0004-lib-var-expand-Change-escape-func-signature-to-retur.patch b/patches/0004-lib-var-expand-Change-escape-func-signature-to-retur.patch
new file mode 100644 (file)
index 0000000..3211d30
--- /dev/null
@@ -0,0 +1,305 @@
+From d7a040f5b0230368dd97112daf7a710446fdfd16 Mon Sep 17 00:00:00 2001
+From: Aki Tuomi <aki.tuomi@open-xchange.com>
+Date: Thu, 21 May 2026 11:00:56 +0300
+Subject: [PATCH 4/6] lib-var-expand: Change escape func signature to return
+ int with error_r
+
+Wire error propagation in var_expand_program_execute_one_real() so a
+failing escape function causes the expansion to return -1.
+
+Update all implementations (passdb-sql, userdb-sql, db-ldap, dict-ldap,
+auth-request-var-expand) and replace the unsafe auth_request_escape_func_t
+cast in auth-request-var-expand.c with a proper bridge wrapper.
+---
+ src/auth/auth-request-var-expand.c      | 13 +++++++++++--
+ src/auth/db-ldap.c                      | 12 ++++++++----
+ src/auth/db-ldap.h                      |  4 ++--
+ src/auth/passdb-sql.c                   |  6 ++++--
+ src/auth/test-auth-request-var-expand.c | 16 ++++++++++++----
+ src/auth/test-ldap.c                    |  5 +++--
+ src/auth/userdb-sql.c                   |  6 ++++--
+ src/lib-dict-backend/dict-ldap.c        |  6 ++++--
+ src/lib-var-expand/expansion-filter.c   |  7 +++++--
+ src/lib-var-expand/expansion-program.c  | 11 +++++++----
+ src/lib-var-expand/test-var-expand.c    |  6 ++++--
+ src/lib-var-expand/var-expand.h         |  7 ++++---
+ 12 files changed, 68 insertions(+), 31 deletions(-)
+
+Index: trixie/src/auth/auth-request-var-expand.c
+===================================================================
+--- trixie.orig/src/auth/auth-request-var-expand.c
++++ trixie/src/auth/auth-request-var-expand.c
+@@ -55,6 +55,15 @@ escape_none(const char *string,
+       return string;
+ }
+ 
++static int
++auth_request_escape_wrapper(const char *input, const char **output_r,
++                          void *context, const char **error_r ATTR_UNUSED)
++{
++      const struct auth_request_var_expand_ctx *ctx = context;
++      *output_r = ctx->escape_func(input, ctx->auth_request);
++      return 0;
++}
++
+ const char *
+ auth_request_str_escape(const char *string,
+                       const struct auth_request *request ATTR_UNUSED)
+@@ -242,9 +251,9 @@ int auth_request_var_expand_with_table(s
+       const struct var_expand_params params = {
+               .table = table,
+               .providers = auth_request_var_expand_providers,
+-              .escape_func = (var_expand_escape_func_t *)ctx.escape_func,
++              .escape_func = auth_request_escape_wrapper,
+               .context = &ctx,
+-              .escape_context = (void *)auth_request,
++              .escape_context = &ctx,
+               .event = auth_request->event,
+       };
+ 
+Index: trixie/src/auth/db-ldap.c
+===================================================================
+--- trixie.orig/src/auth/db-ldap.c
++++ trixie/src/auth/db-ldap.c
+@@ -1138,7 +1138,8 @@ void db_ldap_get_attribute_names(pool_t
+               *sensitive_r = array_front(&sensitive_attr_names);
+ }
+ 
+-const char *ldap_escape(const char *input, void *context ATTR_UNUSED)
++int ldap_escape(const char *input, const char **output_r,
++              void *context ATTR_UNUSED, const char **error_r ATTR_UNUSED)
+ {
+       /* This function escapes both LDAP filters and LDAP DNs. This works,
+          because both allow using the method of escaping any characters.
+@@ -1195,8 +1196,10 @@ const char *ldap_escape(const char *inpu
+                          (required by DN) */
+                       if (pos > 0 && input[pos - 1] == ' ')
+                               pos--;
+-                      else
+-                              return input;
++                      else {
++                              *output_r = input;
++                              return 0;
++                      }
+               }
+       }
+ 
+@@ -1211,7 +1214,8 @@ const char *ldap_escape(const char *inpu
+                       pos--;
+       } while (pos < input_len);
+       str_append_data(str, input, pos);
+-      return str_c(str);
++      *output_r = str_c(str);
++      return 0;
+ }
+ 
+ static bool
+Index: trixie/src/auth/db-ldap.h
+===================================================================
+--- trixie.orig/src/auth/db-ldap.h
++++ trixie/src/auth/db-ldap.h
+@@ -169,8 +169,8 @@ void db_ldap_connect_delayed(struct ldap
+ 
+ void db_ldap_enable_input(struct ldap_connection *conn, bool enable);
+ 
+-const char *ldap_dn_escape(const char *str, void *context);
+-const char *ldap_escape(const char *str, void *context);
++int ldap_dn_escape(const char *str, const char **output_r, void *context, const char **error_r);
++int ldap_escape(const char *str, const char **output_r, void *context, const char **error_r);
+ const char *ldap_get_error(struct ldap_connection *conn);
+ 
+ struct db_ldap_result_iterate_context *
+Index: trixie/src/auth/passdb-sql.c
+===================================================================
+--- trixie.orig/src/auth/passdb-sql.c
++++ trixie/src/auth/passdb-sql.c
+@@ -167,10 +167,12 @@ static void sql_query_callback(struct sq
+       auth_request_unref(&auth_request);
+ }
+ 
+-static const char *passdb_sql_escape(const char *str, void *context)
++static int passdb_sql_escape(const char *str, const char **output_r,
++                           void *context, const char **error_r ATTR_UNUSED)
+ {
+       struct sql_db *db = context;
+-      return sql_escape_string(db, str);
++      *output_r = sql_escape_string(db, str);
++      return 0;
+ }
+ 
+ static void sql_lookup_pass(struct passdb_sql_request *sql_request)
+Index: trixie/src/auth/test-auth-request-var-expand.c
+===================================================================
+--- trixie.orig/src/auth/test-auth-request-var-expand.c
++++ trixie/src/auth/test-auth-request-var-expand.c
+@@ -70,6 +70,14 @@ test_escape(const char *string, const st
+       return dest;
+ }
+ 
++static int
++test_escape_varexpand(const char *string, const char **output_r,
++                    void *context, const char **error_r ATTR_UNUSED)
++{
++      *output_r = test_escape(string, context);
++      return 0;
++}
++
+ static bool test_empty_request(string_t *str, const char *input)
+ {
+       const struct var_expand_params params = {
+@@ -102,7 +110,7 @@ static void test_auth_request_var_expand
+ 
+       const struct var_expand_params params = {
+               .table = auth_request_get_var_expand_table(&test_request),
+-              .escape_func = (var_expand_escape_func_t *)test_escape,
++              .escape_func = test_escape_varexpand,
+               .escape_context = &test_request,
+       };
+ 
+@@ -129,7 +137,7 @@ static void test_auth_request_var_expand
+ 
+       struct var_expand_params params = {
+               .table = auth_request_get_var_expand_table(&test_request),
+-              .escape_func = (var_expand_escape_func_t *)test_escape,
++              .escape_func = test_escape_varexpand,
+               .escape_context = &test_request
+       };
+       test_assert(var_expand(str, test_input, &params, &error) == 0);
+@@ -169,7 +177,7 @@ static void test_auth_request_var_expand
+ 
+       const struct var_expand_params params = {
+               .table = auth_request_get_var_expand_table(&test_request),
+-              .escape_func = (var_expand_escape_func_t *)test_escape,
++              .escape_func = test_escape_varexpand,
+               .escape_context = &test_request,
+       };
+ 
+@@ -200,7 +208,7 @@ static void test_auth_request_var_expand
+               test_request.fields.user = t_strdup_noconst(tests[i].username);
+               const struct var_expand_params params = {
+                       .table = auth_request_get_var_expand_table(&test_request),
+-                      .escape_func = (var_expand_escape_func_t *)test_escape,
++                      .escape_func = test_escape_varexpand,
+                       .escape_context = &test_request,
+               };
+               str_truncate(str, 0);
+Index: trixie/src/auth/test-ldap.c
+===================================================================
+--- trixie.orig/src/auth/test-ldap.c
++++ trixie/src/auth/test-ldap.c
+@@ -25,8 +25,9 @@ static void test_ldap_escape(void)
+       };
+       test_begin("ldap_escape()");
+       for (unsigned int i = 0; i < N_ELEMENTS(tests); i++) {
+-              test_assert_strcmp_idx(ldap_escape(tests[i].input, NULL),
+-                                     tests[i].output, i);
++              const char *output, *error;
++              test_assert(ldap_escape(tests[i].input, &output, NULL, &error) == 0);
++              test_assert_strcmp_idx(output, tests[i].output, i);
+       }
+       test_end();
+ }
+Index: trixie/src/auth/userdb-sql.c
+===================================================================
+--- trixie.orig/src/auth/userdb-sql.c
++++ trixie/src/auth/userdb-sql.c
+@@ -112,10 +112,12 @@ static void sql_query_callback(struct sq
+       i_free(sql_request);
+ }
+ 
+-static const char *userdb_sql_escape(const char *str, void *context)
++static int userdb_sql_escape(const char *str, const char **output_r,
++                           void *context, const char **error_r ATTR_UNUSED)
+ {
+       struct sql_db *db = context;
+-      return sql_escape_string(db, str);
++      *output_r = sql_escape_string(db, str);
++      return 0;
+ }
+ 
+ static void userdb_sql_lookup(struct auth_request *auth_request,
+Index: trixie/src/lib-dict-backend/dict-ldap.c
+===================================================================
+--- trixie.orig/src/lib-dict-backend/dict-ldap.c
++++ trixie/src/lib-dict-backend/dict-ldap.c
+@@ -139,7 +139,8 @@ int dict_ldap_connect(struct ldap_dict *
+ #define IS_LDAP_ESCAPED_CHAR(c) \
+       ((((unsigned char)(c)) & 0x80) != 0 || strchr(LDAP_ESCAPE_CHARS, (c)) != NULL)
+ 
+-static const char *ldap_escape(const char *str, void *context ATTR_UNUSED)
++static int ldap_escape(const char *str, const char **output_r,
++                     void *context ATTR_UNUSED, const char **error_r ATTR_UNUSED)
+ {
+       string_t *ret = NULL;
+ 
+@@ -154,7 +155,8 @@ static const char *ldap_escape(const cha
+                       str_append_c(ret, *p);
+       }
+ 
+-      return ret == NULL ? str : str_c(ret);
++      *output_r = ret == NULL ? str : str_c(ret);
++      return 0;
+ }
+ 
+ static
+Index: trixie/src/lib-var-expand/expansion-program.c
+===================================================================
+--- trixie.orig/src/lib-var-expand/expansion-program.c
++++ trixie/src/lib-var-expand/expansion-program.c
+@@ -142,9 +142,11 @@ int var_expand_program_execute(string_t
+               if (state.transfer_set) {
+                       if (!program->only_literal && !state.transfer_safe &&
+                           params->escape_func != NULL) {
+-                              str_append(state.result,
+-                                         params->escape_func(str_c(state.transfer),
+-                                                             params->escape_context));
++                              const char *escaped;
++                              if (params->escape_func(str_c(state.transfer), &escaped,
++                                                      params->escape_context, error_r) < 0)
++                                      return -1;
++                              str_append(state.result, escaped);
+                       } else
+                               str_append_str(state.result, state.transfer);
+               } else {
+Index: trixie/src/lib-var-expand/test-var-expand.c
+===================================================================
+--- trixie.orig/src/lib-var-expand/test-var-expand.c
++++ trixie/src/lib-var-expand/test-var-expand.c
+@@ -586,7 +586,8 @@ static void test_var_expand_tables_arr(v
+       test_end();
+ }
+ 
+-static const char *test_escape(const char *str, void *context)
++static int test_escape(const char *str, const char **output_r,
++                     void *context, const char **error_r ATTR_UNUSED)
+ {
+       const char *escape_chars = context;
+       string_t *dest = t_str_new(strlen(str) + 2);
+@@ -601,7 +602,8 @@ static const char *test_escape(const cha
+               }
+       }
+       str_append_c(dest, '\'');
+-      return str_c(dest);
++      *output_r = str_c(dest);
++      return 0;
+ }
+ 
+ static void test_var_expand_escape(void)
+Index: trixie/src/lib-var-expand/var-expand.h
+===================================================================
+--- trixie.orig/src/lib-var-expand/var-expand.h
++++ trixie/src/lib-var-expand/var-expand.h
+@@ -11,9 +11,10 @@
+ */
+ typedef int value_provider_func_t(const char *key, const char **value_r,
+                                 void *context, const char **error_r);
+-/* Used for escaping values, gets given string to escape and context,
+-   must return escaped string. */
+-typedef const char *var_expand_escape_func_t(const char *str, void *context);
++/* Used for escaping values. On success sets output_r and returns 0.
++   On failure sets error_r and returns -1. */
++typedef int var_expand_escape_func_t(const char *input, const char **output_r,
++                                   void *context, const char **error_r);
+ 
+ struct var_expand_parser_state;
+ struct var_expand_program;
diff --git a/patches/0004-submission-login-client-authenticate-Reply-421-4.7.0.patch b/patches/0004-submission-login-client-authenticate-Reply-421-4.7.0.patch
new file mode 100644 (file)
index 0000000..f5bbc7a
--- /dev/null
@@ -0,0 +1,51 @@
+From 4e968e6c8614a935a83024ba4ac9a5c9b6c6b7ff Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Thu, 7 May 2026 10:58:12 +0000
+Subject: [PATCH 4/5] submission-login: client-authenticate - Reply 421 4.7.0
+ on mail_max_userip_connections
+
+Until now the connection limit was reported via the same 454 4.7.0 reply
+that is used for generic temporary authentication failures. That makes
+proxies (including Dovecot's own submission proxy) treat the rejection as
+a transient auth error and retry, which is futile when the limit is hit
+and only obscures the actual cause in the proxy log.
+
+Reply with 421 4.7.0 instead. RFC 5321 Section 4.2.1 specifies 421 as
+"service shutting down, closing transmission channel", which is the
+right signal for "do not retry on this connection". The 421 + 4.7.0
+combination is unique among the 421 replies emitted by submission and is
+used by the submission proxy to recognize this specifically as a
+connection-limit reply rather than a generic 421 internal/shutdown.
+---
+ src/submission-login/client-authenticate.c | 19 ++++++++++++++++++-
+ 1 file changed, 18 insertions(+), 1 deletion(-)
+
+Index: trixie/src/submission-login/client-authenticate.c
+===================================================================
+--- trixie.orig/src/submission-login/client-authenticate.c
++++ trixie/src/submission-login/client-authenticate.c
+@@ -151,6 +151,24 @@ void submission_client_auth_result(struc
+                */
+               smtp_server_reply(cmd, 454, "4.7.0", "%s", text);
+               break;
++      case CLIENT_AUTH_RESULT_LIMIT_REACHED:
++              /* The user has too many concurrent connections. Reply with
++                 421 4.7.0: 421 means "service shutting down, closing
++                 transmission channel" (RFC 5321 Section 4.2.1) and
++                 signals the client that retrying on this same connection
++                 is pointless. The proxy uses the 421 + 4.7.0 combination
++                 to recognize this specifically as a connection-limit
++                 response (rather than a generic 421 internal/shutdown
++                 reply) and avoid reconnecting.
++
++                 Use reply_immediate() rather than the queued
++                 smtp_server_reply() because the caller (sasl-server)
++                 immediately tears the connection down after this returns,
++                 which would abort a queued reply before it reaches the
++                 wire. */
++              smtp_server_connection_reply_immediate(subm_client->conn,
++                      421, "4.7.0 %s", text);
++              break;
+       case CLIENT_AUTH_RESULT_ABORTED:
+               /* RFC4954, Section 4:
+ 
diff --git a/patches/0005-imap-Add-imap_compress_on_proxy-hidden-setting.patch b/patches/0005-imap-Add-imap_compress_on_proxy-hidden-setting.patch
new file mode 100644 (file)
index 0000000..b974c65
--- /dev/null
@@ -0,0 +1,62 @@
+From c27391fb8528e95e2b2b30c9694aab1ab4341b14 Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Wed, 4 Jun 2025 17:05:36 +0300
+Subject: [PATCH 5/6] imap: Add imap_compress_on_proxy hidden setting
+
+Keep it disabled by default for now. Once we're sure COMPRESS on proxy
+works properly we'll enable it again.
+---
+ src/imap/cmd-compress.c  | 3 ++-
+ src/imap/imap-settings.c | 2 ++
+ src/imap/imap-settings.h | 1 +
+ 3 files changed, 5 insertions(+), 1 deletion(-)
+
+diff --git a/src/imap/cmd-compress.c b/src/imap/cmd-compress.c
+index 18aa988892..0367cc6f64 100644
+--- a/src/imap/cmd-compress.c
++++ b/src/imap/cmd-compress.c
+@@ -87,7 +87,8 @@ bool cmd_compress(struct client_command_context *cmd)
+               return TRUE;
+       }
+ 
+-      if (client->multiplex_output != NULL) {
++      if (client->multiplex_output != NULL &&
++          client->set->imap_compress_on_proxy) {
+               /* Let imap-login process handle the COMPRESS. It's the one
+                  that will send the tagged reply to the client. */
+               client->compress_handler = handler;
+diff --git a/src/imap/imap-settings.c b/src/imap/imap-settings.c
+index aa0cd747cb..3e2561ccd2 100644
+--- a/src/imap/imap-settings.c
++++ b/src/imap/imap-settings.c
+@@ -81,6 +81,7 @@ static const struct setting_define imap_setting_defines[] = {
+       DEF(ENUM, imap_fetch_failure),
+       DEF(BOOL, imap_metadata),
+       DEF(BOOL, imap_literal_minus),
++      DEF(BOOL_HIDDEN, imap_compress_on_proxy),
+       DEF(BOOL, mail_utf8_extensions),
+ #ifdef BUILD_IMAP_HIBERNATE
+       DEF(TIME, imap_hibernate_timeout),
+@@ -115,6 +116,7 @@ static const struct imap_settings imap_default_settings = {
+       .imap_fetch_failure = "disconnect-immediately:disconnect-after:no-after",
+       .imap_metadata = FALSE,
+       .imap_literal_minus = FALSE,
++      .imap_compress_on_proxy = FALSE,
+       .mail_utf8_extensions = FALSE,
+ #ifdef DOVECOT_PRO_EDITION
+       .imap_hibernate_timeout = 30,
+diff --git a/src/imap/imap-settings.h b/src/imap/imap-settings.h
+index 202ea45a30..347d85cec7 100644
+--- a/src/imap/imap-settings.h
++++ b/src/imap/imap-settings.h
+@@ -34,6 +34,7 @@ struct imap_settings {
+       const char *imap_fetch_failure;
+       bool imap_metadata;
+       bool imap_literal_minus;
++      bool imap_compress_on_proxy;
+       bool mail_utf8_extensions;
+       unsigned int imap_hibernate_timeout;
+       ARRAY_TYPE(const_string) imap_id_send;
+-- 
+2.47.3
+
diff --git a/patches/0005-lib-Add-comments-about-memory-allocations-and-string.patch b/patches/0005-lib-Add-comments-about-memory-allocations-and-string.patch
new file mode 100644 (file)
index 0000000..e2f7363
--- /dev/null
@@ -0,0 +1,81 @@
+From 626a19e307e915b2c545cad226aa57aec8620906 Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Wed, 3 Jun 2026 21:14:51 +0000
+Subject: [PATCH 5/5] lib: Add comments about memory allocations and string
+ functions preserving errno
+
+---
+ src/lib/data-stack.h | 5 +++++
+ src/lib/imem.h       | 3 +++
+ src/lib/mempool.h    | 6 ++++++
+ src/lib/strfuncs.h   | 7 +++++++
+ 4 files changed, 21 insertions(+)
+
+diff --git a/src/lib/data-stack.h b/src/lib/data-stack.h
+index 7ff66c4ef4..e0febe494f 100644
+--- a/src/lib/data-stack.h
++++ b/src/lib/data-stack.h
+@@ -31,6 +31,11 @@
+       overflows.
+ */
+ 
++/* All data stack allocations - t_malloc(), t_malloc0(), t_buffer_get(),
++   t_try_realloc() etc. - preserve errno. It is therefore safe to allocate
++   temporary memory between a failing syscall and reading errno (or
++   formatting "%m"). */
++
+ #ifndef STATIC_CHECKER
+ typedef unsigned int data_stack_frame_t;
+ #else
+diff --git a/src/lib/imem.h b/src/lib/imem.h
+index ed8c2eb581..6635bc018a 100644
+--- a/src/lib/imem.h
++++ b/src/lib/imem.h
+@@ -3,6 +3,9 @@
+ 
+ /* For easy allocation of memory from default memory pool. */
+ 
++/* Like all pool allocations, i_malloc()/i_realloc()/i_free() and the
++   i_strdup*() helpers preserve errno (see mempool.h). */
++
+ extern pool_t default_pool;
+ 
+ #define i_new(type, count) p_new(default_pool, type, count)
+diff --git a/src/lib/mempool.h b/src/lib/mempool.h
+index 3b9872cb20..37ecff4c7c 100644
+--- a/src/lib/mempool.h
++++ b/src/lib/mempool.h
+@@ -19,6 +19,12 @@
+    zeroed, it will cost only a few CPU cycles and may well save some debug
+    time. */
+ 
++/* All pool memory operations - p_malloc(), p_realloc(), p_free() and the
++   p_new()/p_strdup*() helpers built on them - preserve errno. This means an
++   allocation between a failing syscall and reading errno (e.g. when building
++   an error string with "%m") will not clobber errno. The same guarantee holds
++   for the t_* (data stack) and i_* (default pool) allocators. */
++
+ typedef struct pool *pool_t;
+ 
+ struct pool_vfuncs {
+diff --git a/src/lib/strfuncs.h b/src/lib/strfuncs.h
+index 715af58d8e..e6daa14847 100644
+--- a/src/lib/strfuncs.h
++++ b/src/lib/strfuncs.h
+@@ -13,6 +13,13 @@ extern const char *const empty_str_array[];
+ int i_snprintf(char *dest, size_t max_chars, const char *format, ...)
+       ATTR_FORMAT(3, 4);
+ 
++/* The p_/t_/i_ strdup, strconcat and *printf helpers below preserve errno.
++   So this pattern is safe - errno still refers to the failed syscall when
++   "%m" is expanded:
++
++     if (syscall(...) < 0)
++             error = t_strdup_printf("syscall() failed: %m"); */
++
+ char *p_strdup(pool_t pool, const char *str) ATTR_MALLOC;
+ void *p_memdup(pool_t pool, const void *data, size_t size) ATTR_MALLOC;
+ /* return NULL if str = "" */
+-- 
+2.39.5
+
diff --git a/patches/0005-lib-index-mail-index-strmap-Fix-OOB-read-from-trunca.patch b/patches/0005-lib-index-mail-index-strmap-Fix-OOB-read-from-trunca.patch
new file mode 100644 (file)
index 0000000..45b2ff0
--- /dev/null
@@ -0,0 +1,27 @@
+From b0f98d919ca586171c8149e52326e6f0c0ec5252 Mon Sep 17 00:00:00 2001
+From: Aki Tuomi <aki.tuomi@open-xchange.com>
+Date: Mon, 17 Aug 2026 11:13:18 +0300
+Subject: [PATCH 05/12] lib-index: mail-index-strmap - Fix OOB read from
+ truncated record size
+
+---
+ src/lib-index/mail-index-strmap.c | 3 +++
+ 1 file changed, 3 insertions(+)
+
+diff --git a/src/lib-index/mail-index-strmap.c b/src/lib-index/mail-index-strmap.c
+index fb92517068..9ce2de9103 100644
+--- a/src/lib-index/mail-index-strmap.c
++++ b/src/lib-index/mail-index-strmap.c
+@@ -425,6 +425,9 @@ mail_index_strmap_read_rec_first(struct mail_index_strmap_read_context *ctx,
+       if (mail_index_strmap_read_packed(ctx, &n) <= 0)
+               return -1;
+       count = n < 2 ? n + 1 : n;
++      /* check that rec_size fits */
++      if (UINT_MAX / (sizeof(ctx->rec.str_idx) + sizeof(*crc32_r)) < count)
++              return -1;
+       ctx->view->total_ref_count += count;
+ 
+       ctx->rec_size = count * (sizeof(ctx->rec.str_idx) + sizeof(*crc32_r));
+-- 
+2.47.3
+
diff --git a/patches/0005-lib-mail-Add-count-field-to-struct-message_address_l.patch b/patches/0005-lib-mail-Add-count-field-to-struct-message_address_l.patch
new file mode 100644 (file)
index 0000000..6679ee3
--- /dev/null
@@ -0,0 +1,56 @@
+From ef2cddece8101876c0e756536e6ef358c780dc07 Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Fri, 17 Apr 2026 15:46:10 +0200
+Subject: [PATCH 05/14] lib-mail: Add count field to struct
+ message_address_list
+
+Add a count field and increment it in add_address() so callers can read
+the number of parsed addresses directly from the list struct without
+walking the linked list.
+
+Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
+---
+ src/lib-mail/message-address.c   | 1 +
+ src/lib-mail/message-address.h   | 1 +
+ src/lib-mail/message-part-data.c | 1 +
+ 3 files changed, 3 insertions(+)
+
+diff --git a/src/lib-mail/message-address.c b/src/lib-mail/message-address.c
+index 675e652e04..967cc518bf 100644
+--- a/src/lib-mail/message-address.c
++++ b/src/lib-mail/message-address.c
+@@ -30,6 +30,7 @@ static void add_address(struct message_address_parser_context *ctx)
+       i_zero(&ctx->addr);
+ 
+       DLLIST2_APPEND(&ctx->addr_list.head, &ctx->addr_list.tail, addr);
++      ctx->addr_list.count++;
+ }
+ 
+ /* quote with "" and escape all '\', '"' and "'" characters if need */
+diff --git a/src/lib-mail/message-address.h b/src/lib-mail/message-address.h
+index 224f7a7560..8b99ce1401 100644
+--- a/src/lib-mail/message-address.h
++++ b/src/lib-mail/message-address.h
+@@ -33,6 +33,7 @@ struct message_address {
+ 
+ struct message_address_list {
+       struct message_address *head, *tail;
++      unsigned int count;
+ };
+ 
+ /* Parse message addresses from given data. Note that giving an empty string
+diff --git a/src/lib-mail/message-part-data.c b/src/lib-mail/message-part-data.c
+index 895bf88e85..acd50e5210 100644
+--- a/src/lib-mail/message-part-data.c
++++ b/src/lib-mail/message-part-data.c
+@@ -247,6 +247,7 @@ void message_part_envelope_parse_from_header(pool_t pool,
+                  validated while MUA only shows the second From header. */
+               DLLIST2_JOIN(&addr_p->head, &addr_p->tail,
+                            &new_addr.head, &new_addr.tail);
++              addr_p->count += new_addr.count;
+       } else if (str_p != NULL) {
+               *str_p = message_header_strdup(pool, hdr->full_value,
+                                              hdr->full_value_len);
+-- 
+2.47.3
+
diff --git a/patches/0005-lib-sieve-file-storage-Remove-orphan-compiled-binari.patch b/patches/0005-lib-sieve-file-storage-Remove-orphan-compiled-binari.patch
new file mode 100644 (file)
index 0000000..b1114be
--- /dev/null
@@ -0,0 +1,55 @@
+From 88470b80fb4079cd2207bc8c4e986b816218421e Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Tue, 5 May 2026 16:51:27 +0000
+Subject: [PATCH 5/7] lib-sieve: file storage: Remove orphan compiled binaries
+ on delete and rename
+
+DELETESCRIPT and RENAMESCRIPT only operated on the .sieve file and
+left the matching compiled binary (.svbin) on disk. The orphan was
+never referenced again under that name, so it accumulated as wasted
+disk space until something else (manual cleanup, account removal)
+removed it.
+
+Unlink fscript->bin_path alongside the .sieve operation. The unlink is
+best-effort: failures other than ENOENT are logged but do not fail the
+DELETE/RENAME.
+---
+ src/lib-sieve/storage/file/sieve-file-script.c | 18 ++++++++++++++++++
+ 1 file changed, 18 insertions(+)
+
+Index: trixie/pigeonhole/src/lib-sieve/storage/file/sieve-file-script.c
+===================================================================
+--- trixie.orig/pigeonhole/src/lib-sieve/storage/file/sieve-file-script.c
++++ trixie/pigeonhole/src/lib-sieve/storage/file/sieve-file-script.c
+@@ -659,6 +659,14 @@ static int sieve_file_storage_script_del
+                               fscript->path);
+               }
+       }
++
++      /* Best-effort: drop the compiled binary as well so it does not
++         linger as a stale orphan. */
++      if (fscript->bin_path != NULL &&
++          unlink(fscript->bin_path) < 0 && errno != ENOENT) {
++              e_error(script->event,
++                      "unlink(%s) failed: %m", fscript->bin_path);
++      }
+       return ret;
+ }
+ 
+@@ -791,6 +799,16 @@ sieve_file_storage_script_rename(struct
+                                               fscript->path);
+                               }
+ 
++                              /* Drop the old compiled binary; the renamed
++                                 script will recompile under its new name. */
++                              if (fscript->bin_path != NULL &&
++                                  unlink(fscript->bin_path) < 0 &&
++                                  errno != ENOENT) {
++                                      e_error(script->event,
++                                              "unlink(%s) failed: %m",
++                                              fscript->bin_path);
++                              }
++
+                               if (script->name != NULL && *script->name != '\0')
+                                       script->name = p_strdup(script->pool, newname);
+                               fscript->path = p_strdup(script->pool, newpath);
diff --git a/patches/0005-lib-sql-Make-escape_string-return-int-with-error_r-f.patch b/patches/0005-lib-sql-Make-escape_string-return-int-with-error_r-f.patch
new file mode 100644 (file)
index 0000000..6bbd491
--- /dev/null
@@ -0,0 +1,629 @@
+From 4e83f5367d2b897430c91c8be9f0830b16e3d90f Mon Sep 17 00:00:00 2001
+From: Aki Tuomi <aki.tuomi@open-xchange.com>
+Date: Fri, 29 May 2026 11:22:57 +0000
+Subject: [PATCH 5/6] lib-sql: Make escape_string return int with error_r, fail
+ instead of unsafe fallback
+
+Change escape_string driver vfunc and sql_escape_string() to return int
+with separate error_r output parameter. On failure (e.g. not connected),
+return -1 instead of falling back to unsafe escaping.
+
+Move escaping from sql_statement_bind_str() to sql_statement_get_query()
+so errors can be propagated to callers. Add failed_error field to
+sql_transaction_context for deferred error reporting at commit time.
+---
+ src/auth/passdb-sql.c          |   5 +-
+ src/auth/userdb-sql.c          |   5 +-
+ src/lib-sql/driver-cassandra.c |  52 ++++++++++++-----
+ src/lib-sql/driver-mysql.c     |  20 +++----
+ src/lib-sql/driver-pgsql.c     |  27 ++++++---
+ src/lib-sql/driver-sqlite.c    |   8 ++-
+ src/lib-sql/driver-sqlpool.c   |  10 ++--
+ src/lib-sql/driver-test.c      |  27 +++++----
+ src/lib-sql/sql-api-private.h  |   8 ++-
+ src/lib-sql/sql-api.c          | 103 +++++++++++++++++++++++++++------
+ src/lib-sql/sql-api.h          |   3 +-
+ 11 files changed, 188 insertions(+), 80 deletions(-)
+
+Index: trixie/src/auth/passdb-sql.c
+===================================================================
+--- trixie.orig/src/auth/passdb-sql.c
++++ trixie/src/auth/passdb-sql.c
+@@ -168,11 +168,10 @@ static void sql_query_callback(struct sq
+ }
+ 
+ static int passdb_sql_escape(const char *str, const char **output_r,
+-                           void *context, const char **error_r ATTR_UNUSED)
++                           void *context, const char **error_r)
+ {
+       struct sql_db *db = context;
+-      *output_r = sql_escape_string(db, str);
+-      return 0;
++      return sql_escape_string(db, str, output_r, error_r);
+ }
+ 
+ static void sql_lookup_pass(struct passdb_sql_request *sql_request)
+Index: trixie/src/auth/userdb-sql.c
+===================================================================
+--- trixie.orig/src/auth/userdb-sql.c
++++ trixie/src/auth/userdb-sql.c
+@@ -113,11 +113,10 @@ static void sql_query_callback(struct sq
+ }
+ 
+ static int userdb_sql_escape(const char *str, const char **output_r,
+-                           void *context, const char **error_r ATTR_UNUSED)
++                           void *context, const char **error_r)
+ {
+       struct sql_db *db = context;
+-      *output_r = sql_escape_string(db, str);
+-      return 0;
++      return sql_escape_string(db, str, output_r, error_r);
+ }
+ 
+ static void userdb_sql_lookup(struct auth_request *auth_request,
+Index: trixie/src/lib-sql/driver-cassandra.c
+===================================================================
+--- trixie.orig/src/lib-sql/driver-cassandra.c
++++ trixie/src/lib-sql/driver-cassandra.c
+@@ -830,22 +830,26 @@ static void driver_cassandra_disconnect(
+       driver_cassandra_close(db, "Disconnected");
+ }
+ 
+-static const char *
++static int
+ driver_cassandra_escape_string(struct sql_db *db ATTR_UNUSED,
+-                             const char *string)
++                             const char *string, const char **output_r,
++                             const char **error_r ATTR_UNUSED)
+ {
+       string_t *escaped;
+       unsigned int i;
+ 
+-      if (strchr(string, '\'') == NULL)
+-              return string;
++      if (strchr(string, '\'') == NULL) {
++              *output_r = string;
++              return 0;
++      }
+       escaped = t_str_new(strlen(string)+10);
+       for (i = 0; string[i] != '\0'; i++) {
+               if (string[i] == '\'')
+                       str_append_c(escaped, '\'');
+               str_append_c(escaped, string[i]);
+       }
+-      return str_c(escaped);
++      *output_r = str_c(escaped);
++      return 0;
+ }
+ 
+ static void
+@@ -2192,8 +2196,13 @@ static void cassandra_transaction_finish
+               if (stmt->prep == NULL)
+                       have_nonprepared = TRUE;
+               if (stmt->cass_stmt == NULL) {
+-                      stmt->cass_stmt = cass_statement_new(
+-                              sql_statement_get_query(&stmt->stmt), 0);
++                      const char *query, *error;
++                      if (sql_statement_get_query(&stmt->stmt,
++                                                  &query, &error) < 0) {
++                              cassandra_transaction_finish(ctx, error);
++                              return;
++                      }
++                      stmt->cass_stmt = cass_statement_new(query, 0);
+                       if (stmt->timestamp != 0) {
+                               cass_statement_set_timestamp(stmt->cass_stmt,
+                                                            stmt->timestamp);
+@@ -2289,11 +2298,15 @@ driver_cassandra_try_commit_s(struct cas
+               i_panic("cassandra: sql_transaction_commit_s() not supported for prepared statements");
+       }
+ 
++      const char *query, *error;
++      if (sql_statement_get_query(&stmt->stmt, &query, &error) < 0) {
++              transaction_set_failed(ctx, error);
++              return;
++      }
++
+       /* just a single query, send it */
+       driver_cassandra_sync_init(db);
+-      result = driver_cassandra_sync_query(db,
+-                      sql_statement_get_query(&stmt->stmt),
+-                      ctx->query_type);
++      result = driver_cassandra_sync_query(db, query, ctx->query_type);
+       driver_cassandra_sync_deinit(db);
+ 
+       if (sql_result_next_row(result) < 0)
+@@ -2796,8 +2809,14 @@ driver_cassandra_statement_query(struct
+       } else {
+               /* Not a prepared statement. Generate a statement from
+                  the query string. */
+-              stmt->result->statement =
+-                      cass_statement_new(sql_statement_get_query(_stmt), 0);
++              const char *query, *error;
++              if (sql_statement_get_query(_stmt, &query, &error) < 0) {
++                      stmt->result->error = i_strdup(error);
++                      result_finish(stmt->result);
++                      cassandra_sql_statement_free(stmt);
++                      return;
++              }
++              stmt->result->statement = cass_statement_new(query, 0);
+               stmt->result->timestamp = stmt->timestamp;
+               if (stmt->timestamp != 0) {
+                       cass_statement_set_timestamp(stmt->result->statement,
+@@ -2826,8 +2845,13 @@ driver_cassandra_update_stmt(struct sql_
+ 
+       i_assert(affected_rows == NULL);
+ 
+-      if (!driver_cassandra_update_query_type(ctx,
+-                      sql_statement_get_query(_stmt))) {
++      const char *query, *error;
++      if (sql_statement_get_query(_stmt, &query, &error) < 0) {
++              transaction_set_failed(ctx, error);
++              cassandra_sql_statement_free(stmt);
++              return;
++      }
++      if (!driver_cassandra_update_query_type(ctx, query)) {
+               cassandra_sql_statement_free(stmt);
+               return;
+       }
+Index: trixie/src/lib-sql/driver-mysql.c
+===================================================================
+--- trixie.orig/src/lib-sql/driver-mysql.c
++++ trixie/src/lib-sql/driver-mysql.c
+@@ -462,8 +462,9 @@ static int driver_mysql_do_query(struct
+       return -1;
+ }
+ 
+-static const char *
+-driver_mysql_escape_string(struct sql_db *_db, const char *string)
++static int
++driver_mysql_escape_string(struct sql_db *_db, const char *string,
++                         const char **output_r, const char **error_r)
+ {
+       struct mysql_db *db = container_of(_db, struct mysql_db, api);
+       size_t len = strlen(string);
+@@ -475,22 +476,15 @@ driver_mysql_escape_string(struct sql_db
+       }
+ 
+       if (_db->state == SQL_DB_STATE_DISCONNECTED) {
+-              /* FIXME: we don't have a valid connection, so fallback
+-                 to using default escaping. the next query will most
+-                 likely fail anyway so it shouldn't matter that much
+-                 what we return here.. Anyway, this API needs
+-                 changing so that the escaping function could already
+-                 fail the query reliably. */
+-              to = t_buffer_get(len * 2 + 1);
+-              len = mysql_escape_string(to, string, len);
+-              t_buffer_alloc(len + 1);
+-              return to;
++              *error_r = SQL_ERRSTR_NOT_CONNECTED;
++              return -1;
+       }
+ 
+       to = t_buffer_get(len * 2 + 1);
+       len = mysql_real_escape_string(db->mysql, to, string, len);
+       t_buffer_alloc(len + 1);
+-      return to;
++      *output_r = to;
++      return 0;
+ }
+ 
+ static void driver_mysql_exec(struct sql_db *_db, const char *query)
+Index: trixie/src/lib-sql/driver-pgsql.c
+===================================================================
+--- trixie.orig/src/lib-sql/driver-pgsql.c
++++ trixie/src/lib-sql/driver-pgsql.c
+@@ -718,8 +718,9 @@ static void do_query(struct pgsql_result
+       }
+ }
+ 
+-static const char *
+-driver_pgsql_escape_string(struct sql_db *_db, const char *string)
++static int
++driver_pgsql_escape_string(struct sql_db *_db, const char *string,
++                         const char **output_r, const char **error_r)
+ {
+       struct pgsql_db *db = (struct pgsql_db *)_db;
+       size_t len = strlen(string);
+@@ -735,14 +736,24 @@ driver_pgsql_escape_string(struct sql_db
+ 
+               to = t_buffer_get(len * 2 + 1);
+               len = PQescapeStringConn(db->pg, to, string, len, &error);
+-      } else
+-#endif
+-      {
+-              to = t_buffer_get(len * 2 + 1);
+-              len = PQescapeString(to, string, len);
++              if (error != 0) {
++                      *error_r = last_error(db);
++                      return -1;
++              }
++              t_buffer_alloc(len + 1);
++              *output_r = to;
++              return 0;
++      } else {
++              *error_r = SQL_ERRSTR_NOT_CONNECTED;
++              return -1;
+       }
++#else
++      to = t_buffer_get(len * 2 + 1);
++      len = PQescapeString(to, string, len);
+       t_buffer_alloc(len + 1);
+-      return to;
++      *output_r = to;
++      return 0;
++#endif
+ }
+ 
+ static void exec_callback(struct sql_result *_result,
+Index: trixie/src/lib-sql/driver-sqlite.c
+===================================================================
+--- trixie.orig/src/lib-sql/driver-sqlite.c
++++ trixie/src/lib-sql/driver-sqlite.c
+@@ -221,15 +221,17 @@ static void driver_sqlite_deinit_v(struc
+       i_free(db);
+ }
+ 
+-static const char *
++static int
+ driver_sqlite_escape_string(struct sql_db *_db ATTR_UNUSED,
+-                          const char *string)
++                          const char *string, const char **output_r,
++                          const char **error_r ATTR_UNUSED)
+ {
+       const size_t len = strlen(string) * 2 + 1;
+       char *escaped = t_malloc_no0(len);
+       if (sqlite3_snprintf(len, escaped, "%q", string) == NULL)
+               i_unreached();
+-      return escaped;
++      *output_r = escaped;
++      return 0;
+ }
+ 
+ static const char *
+Index: trixie/src/lib-sql/driver-sqlpool.c
+===================================================================
+--- trixie.orig/src/lib-sql/driver-sqlpool.c
++++ trixie/src/lib-sql/driver-sqlpool.c
+@@ -573,8 +573,9 @@ static void driver_sqlpool_disconnect(st
+       driver_sqlpool_abort_requests(db);
+ }
+ 
+-static const char *
+-driver_sqlpool_escape_string(struct sql_db *_db, const char *string)
++static int
++driver_sqlpool_escape_string(struct sql_db *_db, const char *string,
++                           const char **output_r, const char **error_r)
+ {
+       struct sqlpool_db *db = (struct sqlpool_db *)_db;
+       const struct sqlpool_connection *conns;
+@@ -584,11 +585,12 @@ driver_sqlpool_escape_string(struct sql_
+       conns = array_get(&db->all_connections, &count);
+       for (i = 0; i < count; i++) {
+               if (SQL_DB_IS_READY(conns[i].db))
+-                      return sql_escape_string(conns[i].db, string);
++                      return sql_escape_string(conns[i].db, string,
++                                               output_r, error_r);
+       }
+       /* no ready connections. just use the first one (we're guaranteed
+          to always have one) */
+-      return sql_escape_string(conns[0].db, string);
++      return sql_escape_string(conns[0].db, string, output_r, error_r);
+ }
+ 
+ static void driver_sqlpool_timeout(struct sqlpool_db *db)
+Index: trixie/src/lib-sql/driver-test.c
+===================================================================
+--- trixie.orig/src/lib-sql/driver-test.c
++++ trixie/src/lib-sql/driver-test.c
+@@ -33,10 +33,12 @@ static int driver_test_sqlite_init(struc
+ static void driver_test_deinit(struct sql_db *_db);
+ static int driver_test_connect(struct sql_db *_db);
+ static void driver_test_disconnect(struct sql_db *_db);
+-static const char *
+-driver_test_mysql_escape_string(struct sql_db *_db, const char *string);
+-static const char *
+-driver_test_escape_string(struct sql_db *_db, const char *string);
++static int
++driver_test_mysql_escape_string(struct sql_db *_db, const char *string,
++                              const char **output_r, const char **error_r);
++static int
++driver_test_escape_string(struct sql_db *_db, const char *string,
++                        const char **output_r, const char **error_r);
+ static void driver_test_exec(struct sql_db *_db, const char *query);
+ static void driver_test_query(struct sql_db *_db, const char *query,
+                              sql_query_callback_t *callback, void *context);
+@@ -237,9 +239,10 @@ static int driver_test_connect(struct sq
+ static void driver_test_disconnect(struct sql_db *_db ATTR_UNUSED)
+ { }
+ 
+-static const char *
++static int
+ driver_test_mysql_escape_string(struct sql_db *_db ATTR_UNUSED,
+-                               const char *string)
++                              const char *string, const char **output_r,
++                              const char **error_r ATTR_UNUSED)
+ {
+       string_t *esc = t_str_new(strlen(string));
+       for(const char *ptr = string; *ptr != '\0'; ptr++) {
+@@ -248,13 +251,17 @@ driver_test_mysql_escape_string(struct s
+                       str_append_c(esc, '\\');
+               str_append_c(esc, *ptr);
+       }
+-      return str_c(esc);
++      *output_r = str_c(esc);
++      return 0;
+ }
+ 
+-static const char *
+-driver_test_escape_string(struct sql_db *_db ATTR_UNUSED, const char *string)
++static int
++driver_test_escape_string(struct sql_db *_db ATTR_UNUSED, const char *string,
++                        const char **output_r,
++                        const char **error_r ATTR_UNUSED)
+ {
+-      return string;
++      *output_r = string;
++      return 0;
+ }
+ 
+ static void driver_test_exec(struct sql_db *_db, const char *query)
+Index: trixie/src/lib-sql/sql-api-private.h
+===================================================================
+--- trixie.orig/src/lib-sql/sql-api-private.h
++++ trixie/src/lib-sql/sql-api-private.h
+@@ -81,7 +81,8 @@ struct sql_db_vfuncs {
+ 
+       int (*connect)(struct sql_db *db);
+       void (*disconnect)(struct sql_db *db);
+-      const char *(*escape_string)(struct sql_db *db, const char *string);
++      int (*escape_string)(struct sql_db *db, const char *string,
++                           const char **output_r, const char **error_r);
+ 
+       void (*exec)(struct sql_db *db, const char *query);
+       /* Only implement this if the driver can really do asynchronous callbacks,
+@@ -215,6 +216,7 @@ struct sql_statement {
+       pool_t pool;
+       const char *query_template;
+       ARRAY_TYPE(const_string) args;
++      ARRAY(bool) args_need_escaping;
+ 
+       /* Tell the driver to not log this query with expanded values.
+          This works only for prepared statements. */
+@@ -252,6 +254,7 @@ struct sql_transaction_context {
+       /* commit() must use this query list if head is non-NULL. */
+       struct sql_transaction_query *head, *tail;
+ 
++      char *failed_error;
+       bool non_atomic;
+ };
+ 
+@@ -277,7 +280,8 @@ inline static const char *sql_db_table_p
+ void sql_transaction_add_query(struct sql_transaction_context *ctx, pool_t pool,
+                              const char *query, unsigned int *affected_rows);
+ const char *sql_statement_get_log_query(struct sql_statement *stmt);
+-const char *sql_statement_get_query(struct sql_statement *stmt);
++int sql_statement_get_query(struct sql_statement *stmt,
++                          const char **query_r, const char **error_r);
+ 
+ void sql_connection_log_finished(struct sql_db *db);
+ struct event_passthrough *
+Index: trixie/src/lib-sql/sql-api.c
+===================================================================
+--- trixie.orig/src/lib-sql/sql-api.c
++++ trixie/src/lib-sql/sql-api.c
+@@ -98,7 +98,6 @@ static const struct sql_result_vfuncs sq
+       .get_error = sql_result_error_get_error,
+ };
+ 
+-static struct sql_result *sql_result_new_error(const char *error) ATTR_UNUSED;
+ static struct sql_result *sql_result_new_error(const char *error)
+ {
+       struct sql_result_error *result = i_new(struct sql_result_error, 1);
+@@ -327,9 +326,10 @@ void sql_disconnect(struct sql_db *db)
+       db->v.disconnect(db);
+ }
+ 
+-const char *sql_escape_string(struct sql_db *db, const char *string)
++int sql_escape_string(struct sql_db *db, const char *string,
++                    const char **output_r, const char **error_r)
+ {
+-      return db->v.escape_string(db, string);
++      return db->v.escape_string(db, string, output_r, error_r);
+ }
+ 
+ const char *sql_escape_blob(struct sql_db *db,
+@@ -391,19 +391,25 @@ default_sql_statement_init_prepared(stru
+ 
+ const char *sql_statement_get_log_query(struct sql_statement *stmt)
+ {
++      const char *query, *error;
+       if (stmt->no_log_expanded_values)
+               return stmt->query_template;
+-      return sql_statement_get_query(stmt);
++      if (sql_statement_get_query(stmt, &query, &error) < 0)
++              return stmt->query_template;
++      return query;
+ }
+ 
+-const char *sql_statement_get_query(struct sql_statement *stmt)
++int sql_statement_get_query(struct sql_statement *stmt,
++                          const char **query_r, const char **error_r)
+ {
+-      string_t *query = t_str_new(128);
++      string_t *query = str_new(default_pool, 128);
+       const char *const *args;
+-      unsigned int args_count, arg_pos = 0;
++      const bool *need_escaping_flags;
++      unsigned int args_count, need_escaping_count, arg_pos = 0;
+       const char *p0, *p1;
+ 
+       args = array_get(&stmt->args, &args_count);
++      need_escaping_flags = array_get(&stmt->args_need_escaping, &need_escaping_count);
+       p0 = stmt->query_template;
+       while ((p1 = strchr(p0, '?')) != NULL) {
+               /* append until ? */
+@@ -413,7 +419,31 @@ const char *sql_statement_get_query(stru
+                       i_panic("lib-sql: Missing bind for arg #%u in statement: %s",
+                               arg_pos, stmt->query_template);
+               }
+-              str_append(query, args[arg_pos++]);
++              if (arg_pos < need_escaping_count && need_escaping_flags[arg_pos]) {
++                      const char *escaped;
++
++                      /* Escape in a nested data stack frame so the
++                         driver's temporary escape buffer is freed
++                         immediately. The escaped value is appended to the
++                         heap-allocated query before the frame is popped. */
++                      T_BEGIN {
++                              if (sql_escape_string(stmt->db, args[arg_pos],
++                                                    &escaped, error_r) < 0)
++                                      escaped = NULL;
++                              else {
++                                      str_append_c(query, '\'');
++                                      str_append(query, escaped);
++                                      str_append_c(query, '\'');
++                              }
++                      } T_END_PASS_STR_IF(escaped == NULL, error_r);
++                      if (escaped == NULL) {
++                              str_free(&query);
++                              return -1;
++                      }
++              } else {
++                      str_append(query, args[arg_pos]);
++              }
++              arg_pos++;
+               p0 = p1 + 1;
+       }
+       str_append(query, p0);
+@@ -422,23 +452,37 @@ const char *sql_statement_get_query(stru
+               i_panic("lib-sql: Too many bind args (%u) for statement: %s",
+                       args_count, stmt->query_template);
+       }
+-      return str_c(query);
++      *query_r = t_strdup(str_c(query));
++      str_free(&query);
++      return 0;
+ }
+ 
+ static void
+ default_sql_statement_query(struct sql_statement *stmt,
+                           sql_query_callback_t *callback, void *context)
+ {
+-      sql_query(stmt->db, sql_statement_get_query(stmt),
+-                callback, context);
++      const char *query, *error;
++      if (sql_statement_get_query(stmt, &query, &error) < 0) {
++              sql_query_callback_delayed(stmt->db,
++                                         sql_result_new_error(error),
++                                         callback, context);
++              pool_unref(&stmt->pool);
++              return;
++      }
++      sql_query(stmt->db, query, callback, context);
+       pool_unref(&stmt->pool);
+ }
+ 
+ static struct sql_result *
+ default_sql_statement_query_s(struct sql_statement *stmt)
+ {
+-      struct sql_result *result =
+-              sql_query_s(stmt->db, sql_statement_get_query(stmt));
++      const char *query, *error;
++      if (sql_statement_get_query(stmt, &query, &error) < 0) {
++              struct sql_result *result = sql_result_new_error(error);
++              pool_unref(&stmt->pool);
++              return result;
++      }
++      struct sql_result *result = sql_query_s(stmt->db, query);
+       pool_unref(&stmt->pool);
+       return result;
+ }
+@@ -447,8 +491,14 @@ static void default_sql_update_stmt(stru
+                                   struct sql_statement *stmt,
+                                   unsigned int *affected_rows)
+ {
+-      ctx->db->v.update(ctx, sql_statement_get_query(stmt),
+-                        affected_rows);
++      const char *query, *error;
++      if (sql_statement_get_query(stmt, &query, &error) < 0) {
++              if (ctx->failed_error == NULL)
++                      ctx->failed_error = i_strdup(error);
++              pool_unref(&stmt->pool);
++              return;
++      }
++      ctx->db->v.update(ctx, query, affected_rows);
+       pool_unref(&stmt->pool);
+ }
+ 
+@@ -487,6 +537,7 @@ sql_statement_init_fields(struct sql_sta
+ {
+       stmt->db = db;
+       p_array_init(&stmt->args, stmt->pool, 8);
++      p_array_init(&stmt->args_need_escaping, stmt->pool, 8);
+ }
+ 
+ struct sql_statement *
+@@ -545,10 +596,10 @@ void sql_statement_set_no_log_expanded_v
+ void sql_statement_bind_str(struct sql_statement *stmt,
+                           unsigned int column_idx, const char *value)
+ {
+-      const char *escaped_value =
+-              p_strdup_printf(stmt->pool, "'%s'",
+-                              sql_escape_string(stmt->db, value));
+-      array_idx_set(&stmt->args, column_idx, &escaped_value);
++      const char *value_dup = p_strdup(stmt->pool, value);
++      array_idx_set(&stmt->args, column_idx, &value_dup);
++      bool needs_escaping = TRUE;
++      array_idx_set(&stmt->args_need_escaping, column_idx, &needs_escaping);
+ 
+       if (stmt->db->v.statement_bind_str != NULL)
+               stmt->db->v.statement_bind_str(stmt, column_idx, value);
+@@ -908,6 +959,13 @@ void sql_transaction_commit(struct sql_t
+       struct sql_db *db = ctx->db;
+       *_ctx = NULL;
+ 
++      if (ctx->failed_error != NULL) {
++              sql_commit_schedule_delayed(db, ctx->failed_error, callback, context);
++              i_free(ctx->failed_error);
++              ctx->db->v.transaction_rollback(ctx);
++              return;
++      }
++
+       if (ctx->db->v.transaction_commit != NULL) {
+               ctx->db->v.transaction_commit(ctx, callback, context);
+               return;
+@@ -924,6 +982,12 @@ int sql_transaction_commit_s(struct sql_
+       struct sql_transaction_context *ctx = *_ctx;
+ 
+       *_ctx = NULL;
++      if (ctx->failed_error != NULL) {
++              *error_r = t_strdup(ctx->failed_error);
++              i_free(ctx->failed_error);
++              ctx->db->v.transaction_rollback(ctx);
++              return -1;
++      }
+       return ctx->db->v.transaction_commit_s(ctx, error_r);
+ }
+ 
+@@ -932,6 +996,7 @@ void sql_transaction_rollback(struct sql
+       struct sql_transaction_context *ctx = *_ctx;
+ 
+       *_ctx = NULL;
++      i_free(ctx->failed_error);
+       ctx->db->v.transaction_rollback(ctx);
+ }
+ 
+Index: trixie/src/lib-sql/sql-api.h
+===================================================================
+--- trixie.orig/src/lib-sql/sql-api.h
++++ trixie/src/lib-sql/sql-api.h
+@@ -114,7 +114,8 @@ int sql_connect(struct sql_db *db);
+ void sql_disconnect(struct sql_db *db);
+ 
+ /* Escape the given string if needed and return it. */
+-const char *sql_escape_string(struct sql_db *db, const char *string);
++int sql_escape_string(struct sql_db *db, const char *string,
++                    const char **output_r, const char **error_r);
+ /* Escape the given data as a string. */
+ const char *sql_escape_blob(struct sql_db *db,
+                           const unsigned char *data, size_t size);
diff --git a/patches/0005-submission-login-submission-proxy-Recognize-421-4.7..patch b/patches/0005-submission-login-submission-proxy-Recognize-421-4.7..patch
new file mode 100644 (file)
index 0000000..4a418d1
--- /dev/null
@@ -0,0 +1,50 @@
+From 8157e395df6b19891cea807fbb1f59e9f8b5a30a Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Thu, 7 May 2026 10:58:24 +0000
+Subject: [PATCH 5/5] submission-login: submission-proxy - Recognize 421 4.7.0
+ as connection-limit reply
+
+When the backend signals that the user's connection limit has been
+reached, do not classify the response as a generic temporary
+authentication failure (which causes the proxy to reconnect). Map it
+instead to LOGIN_PROXY_FAILURE_TYPE_AUTH_LIMIT_REACHED_REPLIED, which
+suppresses retries and reports the failure as proxy_dest_connection_limit
+in the login_aborted event/log line. This mirrors what imap-login does
+for the [LIMIT] response code and pop3-login for [IN-USE].
+
+Other 421 replies (typically server shutdown / fatal error) are mapped
+to LOGIN_PROXY_FAILURE_TYPE_AUTH_REPLIED rather than AUTH_TEMPFAIL: 421
+means "closing transmission channel" (RFC 5321 Section 4.2.1), so a
+reconnect on the same destination is unlikely to help and should not
+happen automatically.
+---
+ src/submission-login/submission-proxy.c | 16 +++++++++++++++-
+ 1 file changed, 15 insertions(+), 1 deletion(-)
+
+Index: dovecot/src/submission-login/submission-proxy.c
+===================================================================
+--- dovecot.orig/src/submission-login/submission-proxy.c
++++ dovecot/src/submission-login/submission-proxy.c
+@@ -694,7 +694,21 @@ int submission_proxy_parse_line(struct c
+          be using only Dovecot as their backend :) */
+       enum login_proxy_failure_type failure_type =
+               LOGIN_PROXY_FAILURE_TYPE_AUTH_REPLIED;
+-      if ((status / 100) == 4)
++      if (status == 421) {
++              /* The backend won't accept further AUTH attempts on this
++                 connection. If it signaled 4.7.0, the user's connection
++                 limit (mail_max_userip_connections) was reached; report
++                 that specifically. Otherwise treat as a generic auth
++                 reply (e.g. backend shutting down). The reply was
++                 prepared from the backend's 421 line; forward it to the
++                 client and detach the AUTH command. */
++              failure_type = (null_strcmp(enh_code, "4.7.0") == 0) ?
++                      LOGIN_PROXY_FAILURE_TYPE_AUTH_LIMIT_REACHED_REPLIED :
++                      LOGIN_PROXY_FAILURE_TYPE_AUTH_REPLIED;
++              i_assert(subm_client->proxy_reply != NULL);
++              smtp_server_reply_submit(subm_client->proxy_reply);
++              subm_client->pending_auth = NULL;
++      } else if ((status / 100) == 4)
+               failure_type = LOGIN_PROXY_FAILURE_TYPE_AUTH_TEMPFAIL;
+       else if (!submission_proxy_handle_redirect(
+                       client, status, enh_code, text, &failure_type, &text)) {
diff --git a/patches/0006-auth-passdb_sql-connect-before-expanding-query-varia.patch b/patches/0006-auth-passdb_sql-connect-before-expanding-query-varia.patch
new file mode 100644 (file)
index 0000000..b7f0d8b
--- /dev/null
@@ -0,0 +1,35 @@
+From abb51a4040fa21314c54a675cf35c4103e92da9c Mon Sep 17 00:00:00 2001
+From: Aki Tuomi <aki.tuomi@open-xchange.com>
+Date: Fri, 29 May 2026 09:28:40 +0300
+Subject: [PATCH 6/6] auth: passdb_sql: connect before expanding query
+ variables
+
+sql_lookup_pass() calls settings_get_params() with an SQL escape func that
+requires a live connection. If the DB is disconnected, the error propagated
+as "Failed to parse configuration" instead of a DB connectivity error.
+
+Call sql_connect() first; fail with "Not connected to database" directly
+if it returns -1.
+---
+ src/auth/passdb-sql.c | 8 ++++++++
+ 1 file changed, 8 insertions(+)
+
+Index: trixie/src/auth/passdb-sql.c
+===================================================================
+--- trixie.orig/src/auth/passdb-sql.c
++++ trixie/src/auth/passdb-sql.c
+@@ -183,6 +183,14 @@ static void sql_lookup_pass(struct passd
+       const struct passdb_sql_settings *set;
+       const char *error;
+ 
++      if (sql_connect(module->db) < 0) {
++              e_error(authdb_event(sql_request->auth_request),
++                      "Not connected to database");
++              sql_request->callback.verify_plain(
++                      PASSDB_RESULT_INTERNAL_FAILURE,
++                      sql_request->auth_request);
++              return;
++      }
+       const struct settings_get_params params = {
+               .escape_func = passdb_sql_escape,
+               .escape_context = module->db,
diff --git a/patches/0006-imap-Reset-DEFLATE-dictionary-after-every-tagged-rep.patch b/patches/0006-imap-Reset-DEFLATE-dictionary-after-every-tagged-rep.patch
new file mode 100644 (file)
index 0000000..46310a4
--- /dev/null
@@ -0,0 +1,61 @@
+From 5ea530ea3677f5fe9d0a7040af87623f3f7af7ce Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Thu, 16 Apr 2026 17:38:53 +0200
+Subject: [PATCH 6/6] imap: Reset DEFLATE dictionary after every tagged reply
+
+Prevents CRIME-style cross-command compression oracle attacks
+(CVE-class: compression side-channel).  Without this fix an observer
+who can inject chosen plaintext into one IMAP command's response can
+measure the compressed size of a subsequent command's response and
+determine whether the secret content matches the injected plaintext.
+
+After each tagged response line is sent, the DEFLATE compression
+dictionary is reset via Z_FULL_FLUSH so that the compression history
+from one command cannot influence the compressed size of the next.
+
+For direct compression (imap_compress_on_proxy=no) the reset is applied
+to the local ostream.  For proxy-mode compression
+(imap_compress_on_proxy=yes) a "dict_reset" command is sent over the
+multiplex side channel to the imap-login process.
+---
+ src/imap/imap-client.c | 17 +++++++++++++++++
+ 1 file changed, 17 insertions(+)
+
+diff --git a/src/imap/imap-client.c b/src/imap/imap-client.c
+index 4c8115f9a0..c44a4d810a 100644
+--- a/src/imap/imap-client.c
++++ b/src/imap/imap-client.c
+@@ -12,6 +12,7 @@
+ #include "istream-concat.h"
+ #include "ostream.h"
+ #include "ostream-multiplex.h"
++#include "ostream-zlib.h"
+ #include "time-util.h"
+ #include "settings.h"
+ #include "master-service.h"
+@@ -712,6 +713,22 @@ client_default_send_tagline(struct client_command_context *cmd, const char *data
+       } T_END;
+ 
+       client->last_output = ioloop_time;
++
++      /* Reset the DEFLATE compression dictionary after every tagged reply so
++         that cross-command compression correlation attacks (CRIME-style) are
++         not possible.  For proxy-mode compression the reset is forwarded to
++         the imap-login process via the side channel; for direct compression
++         it is applied to the local ostream immediately. */
++      if (client->compress_handler != NULL) {
++              if (client->multiplex_output != NULL &&
++                  client->set->imap_compress_on_proxy) {
++                      i_assert(client->side_channel_output != NULL);
++                      o_stream_nsend_str(client->side_channel_output,
++                                         "dict_reset\n");
++              } else {
++                      o_stream_deflate_reset_dict(client->output);
++              }
++      }
+ }
+ 
+ static int
+-- 
+2.47.3
+
diff --git a/patches/0006-lib-index-Rename-crc32-variables-parameters-to-hash-.patch b/patches/0006-lib-index-Rename-crc32-variables-parameters-to-hash-.patch
new file mode 100644 (file)
index 0000000..d561ab7
--- /dev/null
@@ -0,0 +1,366 @@
+From ed3c3d25b3ac571925f19b9b4d0dd8e8a718227e Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Mon, 13 Apr 2026 12:52:15 +0200
+Subject: [PATCH 06/12] lib-index: Rename crc32 variables/parameters to hash in
+ strmap
+
+Pure rename - no functional change. The stored 32-bit value is still
+produced by crc32_str_nonzero(); only the identifiers change to prepare
+for swapping the hash algorithm in the next commit.
+
+recs_crc32 -> recs_hash, hash_key.crc32 -> hash_key.hash,
+crc32_r -> hash_r, local crc32 -> hash, *crc32 -> *hashes.
+---
+ src/lib-index/mail-index-strmap.c | 84 +++++++++++++++----------------
+ 1 file changed, 42 insertions(+), 42 deletions(-)
+
+diff --git a/src/lib-index/mail-index-strmap.c b/src/lib-index/mail-index-strmap.c
+index 9ce2de9103..254080dbb7 100644
+--- a/src/lib-index/mail-index-strmap.c
++++ b/src/lib-index/mail-index-strmap.c
+@@ -31,7 +31,7 @@ struct mail_index_strmap_view {
+       struct mail_index_view *view;
+ 
+       ARRAY_TYPE(mail_index_strmap_rec) recs;
+-      ARRAY(uint32_t) recs_crc32;
++      ARRAY(uint32_t) recs_hash;
+       struct hash2_table *hash;
+ 
+       mail_index_strmap_key_cmp_t *key_compare;
+@@ -74,7 +74,7 @@ struct mail_index_strmap_view_sync {
+ 
+ struct mail_index_strmap_hash_key {
+       const char *str;
+-      uint32_t crc32;
++      uint32_t hash;
+ };
+ 
+ /* renumber the string indexes when highest string idx becomes larger than
+@@ -113,7 +113,7 @@ mail_index_strmap_init(struct mail_index *index, const char *suffix)
+ 
+ static bool
+ mail_index_strmap_read_rec_next(struct mail_index_strmap_read_context *ctx,
+-                              uint32_t *crc32_r);
++                              uint32_t *hash_r);
+ 
+ static void
+ mail_index_strmap_set_syscall_error(struct mail_index_strmap *strmap,
+@@ -162,7 +162,7 @@ static unsigned int mail_index_strmap_hash_key(const void *_key)
+ {
+       const struct mail_index_strmap_hash_key *key = _key;
+ 
+-      return key->crc32;
++      return key->hash;
+ }
+ 
+ static bool
+@@ -197,7 +197,7 @@ mail_index_strmap_view_open(struct mail_index_strmap *strmap,
+       view->next_str_idx = 1;
+ 
+       i_array_init(&view->recs, 64);
+-      i_array_init(&view->recs_crc32, 64);
++      i_array_init(&view->recs_hash, 64);
+       view->hash = hash2_create(0, sizeof(struct mail_index_strmap_rec),
+                                 mail_index_strmap_hash_key,
+                                 mail_index_strmap_hash_cmp, view);
+@@ -212,7 +212,7 @@ void mail_index_strmap_view_close(struct mail_index_strmap_view **_view)
+ 
+       *_view = NULL;
+       array_free(&view->recs);
+-      array_free(&view->recs_crc32);
++      array_free(&view->recs_hash);
+       hash2_destroy(&view->hash);
+       i_free(view);
+ }
+@@ -226,7 +226,7 @@ static void mail_index_strmap_view_reset(struct mail_index_strmap_view *view)
+ {
+       view->remap_cb(NULL, 0, 0, view->cb_context);
+       array_clear(&view->recs);
+-      array_clear(&view->recs_crc32);
++      array_clear(&view->recs_hash);
+       hash2_clear(view->hash);
+ 
+       view->last_added_uid = 0;
+@@ -410,13 +410,13 @@ mail_index_strmap_uid_exists(struct mail_index_strmap_read_context *ctx,
+ 
+ static int
+ mail_index_strmap_read_rec_first(struct mail_index_strmap_read_context *ctx,
+-                               uint32_t *crc32_r)
++                               uint32_t *hash_r)
+ {
+       size_t size;
+       uint32_t n, i, count, str_idx;
+       int ret;
+ 
+-      /* <uid> <n> <crc32>*count <str_idx>*count
++      /* <uid> <n> <hash>*count <str_idx>*count
+          where
+            n = 0 -> count=1 (only Message-ID:)
+            n = 1 -> count=2 (Message-ID: + In-Reply-To:)
+@@ -426,11 +426,11 @@ mail_index_strmap_read_rec_first(struct mail_index_strmap_read_context *ctx,
+               return -1;
+       count = n < 2 ? n + 1 : n;
+       /* check that rec_size fits */
+-      if (UINT_MAX / (sizeof(ctx->rec.str_idx) + sizeof(*crc32_r)) < count)
++      if (UINT_MAX / (sizeof(ctx->rec.str_idx) + sizeof(*hash_r)) < count)
+               return -1;
+       ctx->view->total_ref_count += count;
+ 
+-      ctx->rec_size = count * (sizeof(ctx->rec.str_idx) + sizeof(*crc32_r));
++      ctx->rec_size = count * (sizeof(ctx->rec.str_idx) + sizeof(*hash_r));
+       ret = mail_index_strmap_uid_exists(ctx, ctx->rec.uid);
+       if (ret < 0)
+               return -1;
+@@ -454,10 +454,10 @@ mail_index_strmap_read_rec_first(struct mail_index_strmap_read_context *ctx,
+       /* everything exists. save it. FIXME: these ref_index values
+          are thread index specific, perhaps something more generic
+          should be used some day */
+-      ctx->end = ctx->data + count * sizeof(*crc32_r);
++      ctx->end = ctx->data + count * sizeof(*hash_r);
+ 
+       ctx->next_ref_index = 0;
+-      if (!mail_index_strmap_read_rec_next(ctx, crc32_r))
++      if (!mail_index_strmap_read_rec_next(ctx, hash_r))
+               i_unreached();
+       ctx->next_ref_index = n == 1 ? 1 : 2;
+       return 1;
+@@ -465,7 +465,7 @@ mail_index_strmap_read_rec_first(struct mail_index_strmap_read_context *ctx,
+ 
+ static bool
+ mail_index_strmap_read_rec_next(struct mail_index_strmap_read_context *ctx,
+-                              uint32_t *crc32_r)
++                              uint32_t *hash_r)
+ {
+       if (ctx->data == ctx->end) {
+               i_stream_skip(ctx->view->strmap->input, ctx->rec_size);
+@@ -478,7 +478,7 @@ mail_index_strmap_read_rec_next(struct mail_index_strmap_read_context *ctx,
+ 
+       /* read the record contents */
+       memcpy(&ctx->rec.str_idx, ctx->str_idx_base, sizeof(ctx->rec.str_idx));
+-      memcpy(crc32_r, ctx->data, sizeof(*crc32_r));
++      memcpy(hash_r, ctx->data, sizeof(*hash_r));
+ 
+       ctx->rec.ref_index = ctx->next_ref_index++;
+ 
+@@ -486,7 +486,7 @@ mail_index_strmap_read_rec_next(struct mail_index_strmap_read_context *ctx,
+               ctx->highest_str_idx = ctx->rec.str_idx;
+ 
+       /* get to the next record */
+-      ctx->data += sizeof(*crc32_r);
++      ctx->data += sizeof(*hash_r);
+       ctx->str_idx_base += sizeof(ctx->rec.str_idx);
+       return TRUE;
+ }
+@@ -547,12 +547,12 @@ strmap_read_block_init(struct mail_index_strmap_view *view,
+ 
+ static int
+ strmap_read_block_next(struct mail_index_strmap_read_context *ctx,
+-                     uint32_t *crc32_r)
++                     uint32_t *hash_r)
+ {
+       uint32_t uid_diff;
+       int ret;
+ 
+-      if (mail_index_strmap_read_rec_next(ctx, crc32_r))
++      if (mail_index_strmap_read_rec_next(ctx, hash_r))
+               return 1;
+ 
+       /* get next UID */
+@@ -565,7 +565,7 @@ strmap_read_block_next(struct mail_index_strmap_read_context *ctx,
+                       return -1;
+ 
+               ctx->rec.uid += uid_diff;
+-              ret = mail_index_strmap_read_rec_first(ctx, crc32_r);
++              ret = mail_index_strmap_read_rec_first(ctx, hash_r);
+       } while (ret == 0);
+       return ret;
+ }
+@@ -635,12 +635,12 @@ strmap_view_sync_handle_conflict(struct mail_index_strmap_read_context *ctx,
+ 
+ static int
+ strmap_view_sync_block_check_conflicts(struct mail_index_strmap_read_context *ctx,
+-                                     uint32_t crc32)
++                                     uint32_t hash)
+ {
+       struct mail_index_strmap_rec *hash_rec;
+       struct hash2_iter iter;
+ 
+-      if (crc32 == 0) {
++      if (hash == 0) {
+               /* unique string - there are no conflicts */
+               return 0;
+       }
+@@ -657,9 +657,9 @@ strmap_view_sync_block_check_conflicts(struct mail_index_strmap_read_context *ct
+       strmap index until X has been expunged. */
+       i_zero(&iter);
+       while ((hash_rec = hash2_iterate(ctx->view->hash,
+-                                       crc32, &iter)) != NULL &&
++                                       hash, &iter)) != NULL &&
+              hash_rec->str_idx != ctx->rec.str_idx) {
+-              /* CRC32 matches, but string index doesn't */
++              /* hash matches, but string index doesn't */
+               if (!strmap_view_sync_handle_conflict(ctx, hash_rec, &iter)) {
+                       ctx->lost_expunged_uid = hash_rec->uid;
+                       return -1;
+@@ -672,10 +672,10 @@ static int
+ mail_index_strmap_view_sync_block(struct mail_index_strmap_read_context *ctx)
+ {
+       struct mail_index_strmap_rec *hash_rec;
+-      uint32_t crc32, prev_uid = 0;
++      uint32_t hash, prev_uid = 0;
+       int ret;
+ 
+-      while ((ret = strmap_read_block_next(ctx, &crc32)) > 0) {
++      while ((ret = strmap_read_block_next(ctx, &hash)) > 0) {
+               if (ctx->rec.uid <= ctx->view->last_added_uid) {
+                       if (ctx->rec.uid < ctx->view->last_added_uid ||
+                           prev_uid != ctx->rec.uid) {
+@@ -685,7 +685,7 @@ mail_index_strmap_view_sync_block(struct mail_index_strmap_read_context *ctx)
+               }
+               prev_uid = ctx->rec.uid;
+ 
+-              if (strmap_view_sync_block_check_conflicts(ctx, crc32) < 0) {
++              if (strmap_view_sync_block_check_conflicts(ctx, hash) < 0) {
+                       ret = -1;
+                       break;
+               }
+@@ -693,10 +693,10 @@ mail_index_strmap_view_sync_block(struct mail_index_strmap_read_context *ctx)
+ 
+               /* add the record to records array */
+               array_push_back(&ctx->view->recs, &ctx->rec);
+-              array_push_back(&ctx->view->recs_crc32, &crc32);
++              array_push_back(&ctx->view->recs_hash, &hash);
+ 
+               /* add a separate copy of the record to hash */
+-              hash_rec = hash2_insert_hash(ctx->view->hash, crc32);
++              hash_rec = hash2_insert_hash(ctx->view->hash, hash);
+               memcpy(hash_rec, &ctx->rec, sizeof(*hash_rec));
+       }
+       return strmap_read_block_deinit(ctx, ret, TRUE);
+@@ -764,7 +764,7 @@ void mail_index_strmap_view_sync_add(struct mail_index_strmap_view_sync *sync,
+                 ref_index > view->last_ref_index));
+ 
+       hash_key.str = key;
+-      hash_key.crc32 = crc32_str_nonzero(key);
++      hash_key.hash = crc32_str_nonzero(key);
+ 
+       old_rec = hash2_lookup(view->hash, &hash_key);
+       if (old_rec != NULL) {
+@@ -781,7 +781,7 @@ void mail_index_strmap_view_sync_add(struct mail_index_strmap_view_sync *sync,
+       rec->ref_index = ref_index;
+       rec->str_idx = str_idx;
+       array_push_back(&view->recs, rec);
+-      array_push_back(&view->recs_crc32, &hash_key.crc32);
++      array_push_back(&view->recs_hash, &hash_key.hash);
+ 
+       view->last_added_uid = uid;
+       view->last_ref_index = ref_index;
+@@ -802,7 +802,7 @@ void mail_index_strmap_view_sync_add_unique(struct mail_index_strmap_view_sync *
+       rec.ref_index = ref_index;
+       rec.str_idx = view->next_str_idx++;
+       array_push_back(&view->recs, &rec);
+-      array_append_zero(&view->recs_crc32);
++      array_append_zero(&view->recs_hash);
+ 
+       view->last_added_uid = uid;
+       view->last_ref_index = ref_index;
+@@ -820,7 +820,7 @@ static void mail_index_strmap_view_renumber(struct mail_index_strmap_view *view)
+ {
+       struct mail_index_strmap_read_context ctx;
+       struct mail_index_strmap_rec *recs, *hash_rec;
+-      uint32_t prev_uid, str_idx, *recs_crc32, *renumber_map;
++      uint32_t prev_uid, str_idx, *recs_hash, *renumber_map;
+       unsigned int i, dest, count, count2;
+       int ret;
+ 
+@@ -833,7 +833,7 @@ static void mail_index_strmap_view_renumber(struct mail_index_strmap_view *view)
+       renumber_map = i_new(uint32_t, view->next_str_idx);
+       str_idx = 0; prev_uid = 0;
+       recs = array_get_modifiable(&view->recs, &count);
+-      recs_crc32 = array_get_modifiable(&view->recs_crc32, &count2);
++      recs_hash = array_get_modifiable(&view->recs_hash, &count2);
+       i_assert(count == count2);
+ 
+       for (i = dest = 0; i < count; ) {
+@@ -856,13 +856,13 @@ static void mail_index_strmap_view_renumber(struct mail_index_strmap_view *view)
+                       renumber_map[recs[i].str_idx] = ++str_idx;
+               if (i != dest) {
+                       recs[dest] = recs[i];
+-                      recs_crc32[dest] = recs_crc32[i];
++                      recs_hash[dest] = recs_hash[i];
+               }
+               i++; dest++;
+       }
+       i_assert(renumber_map[0] == 0);
+       array_delete(&view->recs, dest, i-dest);
+-      array_delete(&view->recs_crc32, dest, i-dest);
++      array_delete(&view->recs_hash, dest, i-dest);
+       mail_index_strmap_zero_terminate(view);
+ 
+       /* notify caller of the renumbering */
+@@ -875,7 +875,7 @@ static void mail_index_strmap_view_renumber(struct mail_index_strmap_view *view)
+       hash2_clear(view->hash);
+       for (i = 0; i < count; i++) {
+               recs[i].str_idx = renumber_map[recs[i].str_idx];
+-              hash_rec = hash2_insert_hash(view->hash, recs_crc32[i]);
++              hash_rec = hash2_insert_hash(view->hash, recs_hash[i]);
+               memcpy(hash_rec, &recs[i], sizeof(*hash_rec));
+       }
+ 
+@@ -889,7 +889,7 @@ static void mail_index_strmap_write_block(struct mail_index_strmap_view *view,
+                                         unsigned int i, uint32_t base_uid)
+ {
+       const struct mail_index_strmap_rec *recs;
+-      const uint32_t *crc32;
++      const uint32_t *hashes;
+       unsigned int j, n, count, count2, uid_rec_count;
+       uint32_t block_size;
+       uint8_t *p, packed[MAIL_INDEX_PACK_MAX_SIZE*2];
+@@ -902,7 +902,7 @@ static void mail_index_strmap_write_block(struct mail_index_strmap_view *view,
+ 
+       /* write records */
+       recs = array_get(&view->recs, &count);
+-      crc32 = array_get(&view->recs_crc32, &count2);
++      hashes = array_get(&view->recs_hash, &count2);
+       i_assert(count == count2);
+       while (i < count) {
+               /* @UNSAFE: <uid diff> */
+@@ -919,7 +919,7 @@ static void mail_index_strmap_write_block(struct mail_index_strmap_view *view,
+               }
+               view->total_ref_count += uid_rec_count;
+ 
+-              /* <n> <crc32>*count <str_idx>*count -
++              /* <n> <hash>*count <str_idx>*count -
+                  FIXME: thread index specific code */
+               i_assert(recs[i].ref_index == 0);
+               if (uid_rec_count == 1) {
+@@ -938,7 +938,7 @@ static void mail_index_strmap_write_block(struct mail_index_strmap_view *view,
+               mail_index_pack_num(&p, n);
+               o_stream_nsend(output, packed, p-packed);
+               for (j = 0; j < uid_rec_count; j++)
+-                      o_stream_nsend(output, &crc32[i+j], sizeof(crc32[i+j]));
++                      o_stream_nsend(output, &hashes[i+j], sizeof(hashes[i+j]));
+               for (j = 0; j < uid_rec_count; j++) {
+                       i_assert(j < 2 || recs[i+j].ref_index == j+1);
+                       o_stream_nsend(output, &recs[i+j].str_idx,
+@@ -1095,7 +1095,7 @@ mail_index_strmap_write_append(struct mail_index_strmap_view *view)
+       const struct mail_index_strmap_rec *old_recs;
+       unsigned int i, old_count;
+       struct ostream *output;
+-      uint32_t crc32, next_uid;
++      uint32_t hash, next_uid;
+       bool full_block;
+       int ret;
+ 
+@@ -1134,7 +1134,7 @@ mail_index_strmap_write_append(struct mail_index_strmap_view *view)
+       full_block = TRUE; ret = 0;
+       while (i < old_count &&
+              (ret = strmap_read_block_init(view, &ctx)) > 0) {
+-              while ((ret = strmap_read_block_next(&ctx, &crc32)) > 0) {
++              while ((ret = strmap_read_block_next(&ctx, &hash)) > 0) {
+                       if (ctx.rec.uid != old_recs[i].uid ||
+                           ctx.rec.str_idx != old_recs[i].str_idx) {
+                               /* mismatch */
+-- 
+2.47.3
+
diff --git a/patches/0006-lib-sieve-Log-a-warning-when-cumulative-CPU-exceeds-.patch b/patches/0006-lib-sieve-Log-a-warning-when-cumulative-CPU-exceeds-.patch
new file mode 100644 (file)
index 0000000..43646f8
--- /dev/null
@@ -0,0 +1,55 @@
+From 025233779751d7ba8cff7c615f8e229b604d92a5 Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Wed, 3 Jun 2026 12:25:38 +0000
+Subject: [PATCH 6/7] lib-sieve: Log a warning when cumulative CPU exceeds the
+ limit
+
+sieve_binary_record_resource_usage() returned FALSE when the script's
+cumulative CPU time crossed sieve_max_cpu_time, but the only log line
+on that path was a debug-level "Updated cumulative resource usage"
+emitted on every recording, with no signal at the actual disable
+transition. Operators had to infer the disable from later "Failed to
+open script ... (cumulative resource limit exceeded)" errors on
+subsequent deliveries.
+
+Emit a warning at the transition point, including the cumulative CPU
+usage that triggered it.
+---
+ src/lib-sieve/sieve-binary.c | 17 ++++++++++++++++-
+ 1 file changed, 16 insertions(+), 1 deletion(-)
+
+Index: trixie/pigeonhole/src/lib-sieve/sieve-binary.c
+===================================================================
+--- trixie.orig/pigeonhole/src/lib-sieve/sieve-binary.c
++++ trixie/pigeonhole/src/lib-sieve/sieve-binary.c
+@@ -248,6 +248,7 @@ bool sieve_binary_record_resource_usage(
+       struct sieve_binary *sbin, const struct sieve_resource_usage *rusage)
+ {
+       struct sieve_resource_usage rusage_total;
++      struct sieve_script *script;
+ 
+       if (sbin == NULL)
+               return TRUE;
+@@ -262,7 +263,21 @@ bool sieve_binary_record_resource_usage(
+       e_debug(sbin->event, "Updated cumulative resource usage: %s",
+               sieve_resource_usage_get_summary(&rusage_total));
+ 
+-      return sieve_binary_check_resource_usage(sbin);
++      if (!sieve_resource_usage_is_excessive(sbin->svinst, &rusage_total))
++              return TRUE;
++
++      /* Cumulative resource usage is only tracked for personal scripts;
++         all callers gate this on the user's own script. */
++      script = sbin->script;
++      i_assert(script != NULL);
++      i_assert(sieve_storage_is_personal(script->storage));
++
++      e_warning(sbin->event,
++                "Personal sieve processing disabled for script '%s': "
++                "cumulative resource usage limit exceeded (%s)",
++                sieve_script_label(script),
++                sieve_resource_usage_get_summary(&rusage_total));
++      return FALSE;
+ }
+ 
+ void sieve_binary_set_resource_usage(struct sieve_binary *sbin,
diff --git a/patches/0006-lib-storage-mbox-Explicitly-disable-the-header-block.patch b/patches/0006-lib-storage-mbox-Explicitly-disable-the-header-block.patch
new file mode 100644 (file)
index 0000000..4c517be
--- /dev/null
@@ -0,0 +1,50 @@
+From 89ed3baf87b789f55d7513252ef2dead41cf4105 Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Sun, 19 Apr 2026 15:57:08 +0300
+Subject: [PATCH 06/14] lib-storage/mbox: Explicitly disable the header block
+ size limit
+
+mbox_sync_parse_next_mail() appends each header's raw bytes into
+ctx->header and writes them back when rewriting the mbox file. The
+default MESSAGE_HEADER_BLOCK_DEFAULT_MAX_SIZE limit (10 MB) must not
+apply here: any truncation would corrupt the mbox on rewrite.
+Similarly, mbox_sync_parse_match_mail() feeds full raw header bytes
+into the MD5 verifier.
+
+Today the per-chunk hdr->value delivered by message_parse_header_next()
+is not clamped cumulatively, so the limit only bites on full_value and
+unknown headers pass through intact. That is about to change - a
+subsequent commit will clamp hdr->value cumulatively. Explicitly
+setting SIZE_MAX here locks in the intent and prevents a regression.
+
+Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
+---
+ src/lib-storage/index/mbox/mbox-sync-parse.c | 5 +++++
+ 1 file changed, 5 insertions(+)
+
+diff --git a/src/lib-storage/index/mbox/mbox-sync-parse.c b/src/lib-storage/index/mbox/mbox-sync-parse.c
+index 90f032a017..660e9b63d0 100644
+--- a/src/lib-storage/index/mbox/mbox-sync-parse.c
++++ b/src/lib-storage/index/mbox/mbox-sync-parse.c
+@@ -479,6 +479,9 @@ int mbox_sync_parse_next_mail(struct istream *input,
+ 
+         line_start_pos = 0;
+       hdr_ctx = message_parse_header_init(input, NULL, 0);
++      /* Rewriting the mbox requires the full raw header bytes, so disable
++         the default per-header block size limit. */
++      message_parse_header_set_limit(hdr_ctx, SIZE_MAX);
+       while ((ret = message_parse_header_next(hdr_ctx, &hdr)) > 0) {
+               if (hdr->eoh) {
+                       ctx->have_eoh = TRUE;
+@@ -576,6 +579,8 @@ bool mbox_sync_parse_match_mail(struct mbox_mailbox *mbox,
+         mbox_md5_ctx = mbox->md5_v.init();
+ 
+       hdr_ctx = message_parse_header_init(mbox->mbox_stream, NULL, 0);
++      /* MD5 matching must see the full raw header bytes. */
++      message_parse_header_set_limit(hdr_ctx, SIZE_MAX);
+       while ((ret = message_parse_header_next(hdr_ctx, &hdr)) > 0) {
+               if (hdr->eoh)
+                       break;
+-- 
+2.47.3
+
diff --git a/patches/0007-lib-index-Add-keyed-xxh64-strmap-format-v2-gated-by-.patch b/patches/0007-lib-index-Add-keyed-xxh64-strmap-format-v2-gated-by-.patch
new file mode 100644 (file)
index 0000000..76da814
--- /dev/null
@@ -0,0 +1,364 @@
+From ef45dd7c36562ef07cccf6cc4ad245e0041af456 Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Thu, 30 Apr 2026 12:11:57 +0000
+Subject: [PATCH 07/12] lib-index: Add keyed xxh64 strmap format v2, gated by
+ config version
+
+The on-disk hash stored in the strmap file is now optionally a keyed
+xxh64_to_32() with a per-file random 64-bit IV stored in the file
+header.  This replaces the previous plain crc32_str_nonzero() output
+and hardens the strmap against deliberately-collided message-id
+hashes.  The v1 (crc32) format remains fully readable and writable
+so older configurations keep working without forced rebuilds.
+
+On-disk:
+ - v1 header is 8 bytes: version, 3 unused, uid_validity (unchanged).
+ - v2 header is 16 bytes: version, compat_flags, 2 unused, uid_validity,
+   hash_iv.  The first 8 bytes of v2 align with v1 byte-for-byte, so
+   the open path reads 8 bytes, dispatches on version, and reads the
+   trailing 8 bytes only for v2.
+
+Runtime:
+ - strmap->enable_xxh64 selects the format used when creating a new
+   file (or recreating one whose uid_validity has changed).
+ - view->format_version follows the on-disk file when one exists; for
+   fresh files it follows the strmap-wide preference.
+ - Renumber recreate_write() preserves view->format_version, so we
+   never silently migrate an existing file across the threshold while
+   it is still in use - the migration only happens at open time.
+ - When enable_xxh64 is TRUE and the on-disk file is v1, open treats
+   it as a version mismatch: the file is unlinked and the next sync's
+   recreate_write() produces a v2 file.  Below the threshold v1 stays
+   v1 indefinitely.
+ - strmap_hash_str() dispatches per-view, so v1 files keep using
+   crc32 hashes and v2 files use keyed xxh64 even in mixed setups.
+---
+ src/lib-index/mail-index-strmap.c    | 106 ++++++++++++++++++++++++---
+ src/lib-index/mail-index-strmap.h    |  17 ++++-
+ src/lib-master/Makefile.am           |   1 +
+ src/lib-master/storage-version.h     |  22 ++++++
+ src/lib-storage/index/index-thread.c |  12 ++-
+ 5 files changed, 143 insertions(+), 15 deletions(-)
+ create mode 100644 src/lib-master/storage-version.h
+
+Index: trixie/src/lib-index/mail-index-strmap.c
+===================================================================
+--- trixie.orig/src/lib-index/mail-index-strmap.c
++++ trixie/src/lib-index/mail-index-strmap.c
+@@ -8,6 +8,8 @@
+ #include "file-lock.h"
+ #include "file-dotlock.h"
+ #include "crc32.h"
++#include "randgen.h"
++#include "xxh64.h"
+ #include "safe-mkstemp.h"
+ #include "str.h"
+ #include "mail-index-private.h"
+@@ -24,6 +26,12 @@ struct mail_index_strmap {
+       struct file_lock *file_lock;
+       struct dotlock *dotlock;
+       struct dotlock_settings dotlock_settings;
++
++      /* If TRUE, new files are created in v2 format (xxh64-keyed hashes).
++         If FALSE, new files are created in v1 format (crc32). Existing
++         files of either version are always read; on rebuild they are
++         recreated in this preferred format. */
++      bool enable_xxh64;
+ };
+ 
+ struct mail_index_strmap_view {
+@@ -34,6 +42,14 @@ struct mail_index_strmap_view {
+       ARRAY(uint32_t) recs_hash;
+       struct hash2_table *hash;
+ 
++      /* On-disk format version that view's hash values are computed
++         against. v1 = crc32_str_nonzero, v2 = keyed xxh64. Set from the
++         file header when opening an existing file; otherwise from
++         strmap->enable_xxh64. */
++      uint8_t format_version;
++      /* Per-file random IV mixed into the xxh64 hash. Unused for v1. */
++      uint64_t hash_iv;
++
+       mail_index_strmap_key_cmp_t *key_compare;
+       mail_index_strmap_rec_cmp_t *rec_compare;
+       mail_index_strmap_remap_t *remap_cb;
+@@ -92,7 +108,8 @@ static const struct dotlock_settings def
+ };
+ 
+ struct mail_index_strmap *
+-mail_index_strmap_init(struct mail_index *index, const char *suffix)
++mail_index_strmap_init(struct mail_index *index, const char *suffix,
++                     bool enable_xxh64)
+ {
+       struct mail_index_strmap *strmap;
+ 
+@@ -102,6 +119,7 @@ mail_index_strmap_init(struct mail_index
+       strmap->index = index;
+       strmap->path = i_strconcat(index->filepath, suffix, NULL);
+       strmap->fd = -1;
++      strmap->enable_xxh64 = enable_xxh64;
+ 
+       strmap->dotlock_settings = default_dotlock_settings;
+       strmap->dotlock_settings.use_excl_lock =
+@@ -198,6 +216,9 @@ mail_index_strmap_view_open(struct mail_
+ 
+       i_array_init(&view->recs, 64);
+       i_array_init(&view->recs_hash, 64);
++      view->format_version = strmap->enable_xxh64 ?
++              MAIL_INDEX_STRMAP_VERSION_V2 : MAIL_INDEX_STRMAP_VERSION_V1;
++      random_fill(&view->hash_iv, sizeof(view->hash_iv));
+       view->hash = hash2_create(0, sizeof(struct mail_index_strmap_rec),
+                                 mail_index_strmap_hash_key,
+                                 mail_index_strmap_hash_cmp, view);
+@@ -250,7 +271,7 @@ static int mail_index_strmap_open(struct
+       const struct mail_index_header *idx_hdr;
+       struct mail_index_strmap_header hdr;
+       const unsigned char *data;
+-      size_t size;
++      size_t size, hdr_size;
+       int ret;
+ 
+       i_assert(strmap->fd == -1);
+@@ -263,7 +284,12 @@ static int mail_index_strmap_open(struct
+               return -1;
+       }
+       strmap->input = i_stream_create_fd(strmap->fd, SIZE_MAX);
+-      ret = i_stream_read_bytes(strmap->input, &data, &size, sizeof(hdr));
++
++      /* Read the smaller v1 header first; bytes 0..7 have identical
++         layout in both formats so we can decide based on the version
++         byte before reading the v2 tail. */
++      ret = i_stream_read_bytes(strmap->input, &data, &size,
++                                MAIL_INDEX_STRMAP_HEADER_V1_SIZE);
+       if (ret <= 0) {
+               if (ret < 0) {
+                       mail_index_strmap_set_syscall_error(strmap, "read()");
+@@ -274,23 +300,55 @@ static int mail_index_strmap_open(struct
+               }
+               return ret;
+       }
+-      memcpy(&hdr, data, sizeof(hdr));
++
++      i_zero(&hdr);
++      hdr_size = data[0] == MAIL_INDEX_STRMAP_VERSION_V2 ?
++              MAIL_INDEX_STRMAP_HEADER_V2_SIZE :
++              MAIL_INDEX_STRMAP_HEADER_V1_SIZE;
++      if (hdr_size > MAIL_INDEX_STRMAP_HEADER_V1_SIZE) {
++              ret = i_stream_read_bytes(strmap->input, &data, &size, hdr_size);
++              if (ret <= 0) {
++                      if (ret < 0) {
++                              mail_index_strmap_set_syscall_error(strmap, "read()");
++                              mail_index_strmap_close(strmap);
++                      } else {
++                              mail_index_strmap_view_set_corrupted(view);
++                      }
++                      return ret;
++              }
++      }
++      memcpy(&hdr, data, hdr_size);
+ 
+       idx_hdr = mail_index_get_header(view->view);
+-      if (hdr.version != MAIL_INDEX_STRMAP_VERSION ||
+-          hdr.uid_validity != idx_hdr->uid_validity) {
++      uint8_t compat_flags = 0;
++#ifndef WORDS_BIGENDIAN
++      if (hdr.version >= MAIL_INDEX_STRMAP_VERSION_V2)
++              compat_flags |= MAIL_INDEX_COMPAT_LITTLE_ENDIAN;
++#endif
++      if ((hdr.version != MAIL_INDEX_STRMAP_VERSION_V1 &&
++           hdr.version != MAIL_INDEX_STRMAP_VERSION_V2) ||
++          hdr.compat_flags != compat_flags ||
++          hdr.uid_validity != idx_hdr->uid_validity ||
++          (strmap->enable_xxh64 &&
++           hdr.version < MAIL_INDEX_STRMAP_VERSION_V2)) {
+               /* need to rebuild. if we already had something in the strmap,
+-                 we can keep it. */
++                 we can keep it. The enable_xxh64 case forces a v1 file to
++                 be discarded so the next write creates a v2 file - we never
++                 stay on v1 once the dovecot_storage_version threshold has
++                 been reached. */
+               i_unlink(strmap->path);
+               mail_index_strmap_close(strmap);
+               return 0;
+       }
++      view->format_version = hdr.version;
++      view->hash_iv = hdr.version >= MAIL_INDEX_STRMAP_VERSION_V2 ?
++              hdr.hash_iv : 0;
+ 
+       /* we'll read the entire file from the beginning */
+       view->last_added_uid = 0;
+       view->last_read_uid = 0;
+       view->total_ref_count = 0;
+-      view->last_read_block_offset = sizeof(struct mail_index_strmap_header);
++      view->last_read_block_offset = hdr_size;
+       view->next_str_idx = 1;
+ 
+       mail_index_strmap_view_reset(view);
+@@ -750,6 +808,17 @@ static inline uint32_t crc32_str_nonzero
+       return value == 0 ? 1 : value;
+ }
+ 
++static inline uint32_t
++strmap_hash_str(const struct mail_index_strmap_view *view, const char *str)
++{
++      if (view->format_version < MAIL_INDEX_STRMAP_VERSION_V2)
++              return crc32_str_nonzero(str);
++
++      uint32_t value = xxh64_to_32(xxh64_data(str, strlen(str), view->hash_iv));
++      /* 0 is reserved as a sentinel meaning "unique / no hash stored" */
++      return value == 0 ? 1 : value;
++}
++
+ void mail_index_strmap_view_sync_add(struct mail_index_strmap_view_sync *sync,
+                                    uint32_t uid, uint32_t ref_index,
+                                    const char *key)
+@@ -764,7 +833,7 @@ void mail_index_strmap_view_sync_add(str
+                 ref_index > view->last_ref_index));
+ 
+       hash_key.str = key;
+-      hash_key.hash = crc32_str_nonzero(key);
++      hash_key.hash = strmap_hash_str(view, key);
+ 
+       old_rec = hash2_lookup(view->hash, &hash_key);
+       if (old_rec != NULL) {
+@@ -971,14 +1040,29 @@ mail_index_strmap_recreate_write(struct
+ {
+       const struct mail_index_header *idx_hdr;
+       struct mail_index_strmap_header hdr;
++      size_t hdr_size;
+ 
+       idx_hdr = mail_index_get_header(view->view);
+ 
++      /* view->format_version was set either at view_open() time (from the
++         strmap-wide enable_xxh64 preference for fresh files) or in
++         mail_index_strmap_open() from the existing file's header. Reuse
++         it as-is so renumber recreates preserve the file's format. */
++
+       /* write header */
+       i_zero(&hdr);
+-      hdr.version = MAIL_INDEX_STRMAP_VERSION;
++      hdr.version = view->format_version;
+       hdr.uid_validity = idx_hdr->uid_validity;
+-      o_stream_nsend(output, &hdr, sizeof(hdr));
++      if (view->format_version >= MAIL_INDEX_STRMAP_VERSION_V2) {
++#ifndef WORDS_BIGENDIAN
++              hdr.compat_flags |= MAIL_INDEX_COMPAT_LITTLE_ENDIAN;
++#endif
++              hdr.hash_iv = view->hash_iv;
++              hdr_size = MAIL_INDEX_STRMAP_HEADER_V2_SIZE;
++      } else {
++              hdr_size = MAIL_INDEX_STRMAP_HEADER_V1_SIZE;
++      }
++      o_stream_nsend(output, &hdr, hdr_size);
+ 
+       view->total_ref_count = 0;
+       mail_index_strmap_write_block(view, output, 0, 1);
+Index: trixie/src/lib-index/mail-index-strmap.h
+===================================================================
+--- trixie.orig/src/lib-index/mail-index-strmap.h
++++ trixie/src/lib-index/mail-index-strmap.h
+@@ -7,12 +7,22 @@ struct mail_index;
+ struct mail_index_view;
+ 
+ struct mail_index_strmap_header {
+-#define MAIL_INDEX_STRMAP_VERSION 1
++/* On-disk format version. The header struct below describes version 2;
++   version 1 files use the same first 8 bytes (version + 3 unused + uid_validity)
++   without the trailing compat_flags / unused / hash_iv fields. */
++#define MAIL_INDEX_STRMAP_VERSION_V1 1
++#define MAIL_INDEX_STRMAP_VERSION_V2 2
+       uint8_t version;
+-      uint8_t unused[3];
++      uint8_t compat_flags; /* enum mail_index_header_compat_flags, v2+ */
++      uint8_t unused[2];
+ 
+       uint32_t uid_validity;
++
++      /* v2+ fields - not present on disk for v1 files. */
++      uint64_t hash_iv; /* per-file random IV mixed into the xxh64 hash */
+ };
++#define MAIL_INDEX_STRMAP_HEADER_V1_SIZE 8
++#define MAIL_INDEX_STRMAP_HEADER_V2_SIZE sizeof(struct mail_index_strmap_header)
+ 
+ struct mail_index_strmap_rec {
+       uint32_t uid;
+@@ -40,7 +50,8 @@ typedef void mail_index_strmap_remap_t(c
+                                      unsigned int new_count, void *context);
+ 
+ struct mail_index_strmap *
+-mail_index_strmap_init(struct mail_index *index, const char *suffix);
++mail_index_strmap_init(struct mail_index *index, const char *suffix,
++                     bool enable_xxh64);
+ void mail_index_strmap_deinit(struct mail_index_strmap **strmap);
+ 
+ /* Returns strmap records and hash that can be used for read-only access.
+Index: trixie/src/lib-master/Makefile.am
+===================================================================
+--- trixie.orig/src/lib-master/Makefile.am
++++ trixie/src/lib-master/Makefile.am
+@@ -39,6 +39,7 @@ headers = \
+       master-service-ssl.h \
+       service-settings.h \
+       stats-client.h \
++      storage-version.h \
+       syslog-util.h
+ 
+ pkginc_libdir=$(pkgincludedir)
+Index: trixie/src/lib-master/storage-version.h
+===================================================================
+--- /dev/null
++++ trixie/src/lib-master/storage-version.h
+@@ -0,0 +1,22 @@
++#ifndef STORAGE_VERSION_H
++#define STORAGE_VERSION_H
++
++#include "version.h"
++
++/* Returns TRUE if dovecot_storage_version selects the v2 (keyed xxh64)
++   mail_index_strmap on-disk format. */
++static inline bool
++storage_version_has_mail_index_strmap_v2(const char *version)
++{
++      if (version == NULL) {
++              /* unit test */
++              return TRUE;
++      }
++#ifdef DOVECOT_PRO_EDITION
++      return version_cmp(version, "3.1.6") >= 0;
++#else
++      return version_cmp(version, "2.4.0") >= 0;
++#endif
++}
++
++#endif
+Index: trixie/src/lib-storage/index/index-thread.c
+===================================================================
+--- trixie.orig/src/lib-storage/index/index-thread.c
++++ trixie/src/lib-storage/index/index-thread.c
+@@ -7,6 +7,9 @@
+ #include "bsearch-insert-pos.h"
+ #include "hash2.h"
+ #include "message-id.h"
++#include "master-service.h"
++#include "master-service-settings.h"
++#include "storage-version.h"
+ #include "mail-search.h"
+ #include "mail-search-build.h"
+ #include "mailbox-search-result-private.h"
+@@ -668,8 +671,15 @@ void index_thread_mailbox_opened(struct
+       box->v.close = mail_thread_mailbox_close;
+       box->v.free = mail_thread_mailbox_free;
+ 
++      const struct master_service_settings *master_set =
++              master_service_get_service_settings(master_service);
++      bool use_xxh64 = master_set != NULL &&
++              storage_version_has_mail_index_strmap_v2(
++                      master_set->dovecot_storage_version);
++
+       tbox->strmap = mail_index_strmap_init(box->index,
+-                                            MAIL_THREAD_INDEX_SUFFIX);
++                                            MAIL_THREAD_INDEX_SUFFIX,
++                                            use_xxh64);
+       tbox->next_msgid_idx = 1;
+ 
+       tbox->cache = i_new(struct mail_thread_cache, 1);
diff --git a/patches/0007-lib-mail-istream-header-filter-Use-container_of-for-.patch b/patches/0007-lib-mail-istream-header-filter-Use-container_of-for-.patch
new file mode 100644 (file)
index 0000000..e0752d7
--- /dev/null
@@ -0,0 +1,65 @@
+From c5996d464e31ee9950fd40654e9fedcf9f99719e Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Mon, 20 Apr 2026 01:14:02 +0300
+Subject: [PATCH 07/14] lib-mail: istream-header-filter - Use container_of()
+ for struct casts
+
+Replace the C-style downcasts from struct istream_private/iostream_private
+to struct header_filter_istream with container_of(), matching the
+convention used by other istream implementations in tree.
+---
+ src/lib-mail/istream-header-filter.c | 10 +++++-----
+ 1 file changed, 5 insertions(+), 5 deletions(-)
+
+diff --git a/src/lib-mail/istream-header-filter.c b/src/lib-mail/istream-header-filter.c
+index ee94e4f82b..5fee9cf17b 100644
+--- a/src/lib-mail/istream-header-filter.c
++++ b/src/lib-mail/istream-header-filter.c
+@@ -60,7 +60,7 @@ static ssize_t i_stream_header_filter_read(struct istream_private *stream);
+ static void i_stream_header_filter_destroy(struct iostream_private *stream)
+ {
+       struct header_filter_istream *mstream =
+-              (struct header_filter_istream *)stream;
++              container_of(stream, struct header_filter_istream, istream.iostream);
+ 
+       if (mstream->hdr_ctx != NULL)
+               message_parse_header_deinit(&mstream->hdr_ctx);
+@@ -470,7 +470,7 @@ handle_end_body_with_lf(struct header_filter_istream *mstream, ssize_t ret)
+ static ssize_t i_stream_header_filter_read(struct istream_private *stream)
+ {
+       struct header_filter_istream *mstream =
+-              (struct header_filter_istream *)stream;
++              container_of(stream, struct header_filter_istream, istream);
+       uoff_t v_offset;
+       ssize_t ret;
+ 
+@@ -554,7 +554,7 @@ static void i_stream_header_filter_seek(struct istream_private *stream,
+                                       uoff_t v_offset, bool mark ATTR_UNUSED)
+ {
+       struct header_filter_istream *mstream =
+-              (struct header_filter_istream *)stream;
++              container_of(stream, struct header_filter_istream, istream);
+ 
+       if (stream->istream.v_offset == v_offset) {
+               /* just reset the input buffer */
+@@ -604,7 +604,7 @@ static int
+ i_stream_header_filter_stat(struct istream_private *stream, bool exact)
+ {
+       struct header_filter_istream *mstream =
+-              (struct header_filter_istream *)stream;
++              container_of(stream, struct header_filter_istream, istream);
+       const struct stat *st;
+       uoff_t old_offset;
+ 
+@@ -673,7 +673,7 @@ i_stream_header_filter_snapshot(struct istream_private *stream,
+                               struct istream_snapshot *prev_snapshot)
+ {
+       struct header_filter_istream *mstream =
+-              (struct header_filter_istream *)stream;
++              container_of(stream, struct header_filter_istream, istream);
+       struct header_filter_istream_snapshot *snapshot;
+ 
+       if (stream->buffer != mstream->hdr_buf->data) {
+-- 
+2.47.3
+
diff --git a/patches/0007-lib-sieve-Log-an-info-line-when-opening-a-CPU-disabl.patch b/patches/0007-lib-sieve-Log-an-info-line-when-opening-a-CPU-disabl.patch
new file mode 100644 (file)
index 0000000..ee6fbe1
--- /dev/null
@@ -0,0 +1,52 @@
+From 800697eaa3b753d7a449fb7da62f41a21b02925e Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Wed, 3 Jun 2026 12:25:49 +0000
+Subject: [PATCH 7/7] lib-sieve: Log an info line when opening a CPU-disabled
+ script
+
+sieve_binary_check_executable() blocks execution when a script's
+persisted cumulative CPU usage already exceeds sieve_max_cpu_time, but
+only logged the block at debug level. With cumulative tracking now
+sticky across redeliveries via the per-user sieve-rusage file, the
+script can stay blocked indefinitely until the resource_usage_timeout
+decay or admin intervention; operators benefit from a per-attempt
+record of that block at info level.
+
+Promote the existing debug log to e_info and include the cumulative
+CPU usage. The caller continues to emit its own error message on the
+failed open (lda-sieve, imap-sieve, imap-filter-sieve plugin paths).
+---
+ src/lib-sieve/sieve-binary-file.c | 14 +++++++++++---
+ 1 file changed, 11 insertions(+), 3 deletions(-)
+
+Index: trixie/pigeonhole/src/lib-sieve/sieve-binary-file.c
+===================================================================
+--- trixie.orig/pigeonhole/src/lib-sieve/sieve-binary-file.c
++++ trixie/pigeonhole/src/lib-sieve/sieve-binary-file.c
+@@ -935,15 +935,23 @@ int sieve_binary_check_executable(struct
+                                 const char **client_error_r)
+ {
+       struct sieve_resource_usage rusage;
++      struct sieve_script *script = sbin->script;
+ 
+       *client_error_r = NULL;
+       sieve_error_args_init(&error_code_r, NULL);
+ 
+       sieve_binary_get_resource_usage(sbin, &rusage);
+       if (sieve_resource_usage_is_excessive(sbin->svinst, &rusage)) {
+-              e_debug(sbin->event,
+-                      "Binary execution is blocked: "
+-                      "Cumulative resource usage limit exceeded");
++              /* Cumulative resource usage is only tracked for personal
++                 scripts. */
++              i_assert(script != NULL);
++              i_assert(sieve_storage_is_personal(script->storage));
++
++              e_info(sbin->event,
++                     "Personal sieve processing disabled for script '%s': "
++                     "cumulative resource usage limit exceeded (%s)",
++                     sieve_script_label(script),
++                     sieve_resource_usage_get_summary(&rusage));
+               *error_code_r = SIEVE_ERROR_RESOURCE_LIMIT;
+               *client_error_r = "cumulative resource usage limit exceeded";
+               return 0;
diff --git a/patches/0007-lib-sql-Run-sql-statement-queries-in-their-own-data-.patch b/patches/0007-lib-sql-Run-sql-statement-queries-in-their-own-data-.patch
new file mode 100644 (file)
index 0000000..a1175af
--- /dev/null
@@ -0,0 +1,104 @@
+From 30ec9bdcb445011801229a825c1d5a443397463f Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Thu, 11 Jun 2026 20:02:15 +0000
+Subject: [PATCH] lib-sql: Run sql statement queries in their own data stack
+ frame
+
+---
+ src/lib-sql/sql-api.c | 62 +++++++++++++++++++++++++------------------
+ 1 file changed, 36 insertions(+), 26 deletions(-)
+
+Index: trixie/src/lib-sql/sql-api.c
+===================================================================
+--- trixie.orig/src/lib-sql/sql-api.c
++++ trixie/src/lib-sql/sql-api.c
+@@ -657,31 +657,37 @@ void sql_statement_query(struct sql_stat
+       struct sql_statement *stmt = *_stmt;
+       *_stmt = NULL;
+ 
+-      if (stmt->db->v.statement_query != NULL)
+-              stmt->db->v.statement_query(stmt, callback, context);
+-      else if (stmt->db->v.statement_query_s != NULL) {
+-              struct sql_db *db = stmt->db;
+-              struct sql_query_result_delayed *cb =
+-                      i_new(struct sql_query_result_delayed, 1);
+-              cb->db = db;
+-              cb->callback = callback;
+-              cb->context = context;
+-              cb->result = sql_statement_query_s(&stmt);
+-              cb->to = timeout_add_short(0, sql_query_delayed_callback, cb);
+-              DLLIST_PREPEND(&db->query_delayed_list, cb);
+-      } else
+-              default_sql_statement_query(stmt, callback, context);
++      T_BEGIN {
++              if (stmt->db->v.statement_query != NULL)
++                      stmt->db->v.statement_query(stmt, callback, context);
++              else if (stmt->db->v.statement_query_s != NULL) {
++                      struct sql_db *db = stmt->db;
++                      struct sql_query_result_delayed *cb =
++                              i_new(struct sql_query_result_delayed, 1);
++                      cb->db = db;
++                      cb->callback = callback;
++                      cb->context = context;
++                      cb->result = sql_statement_query_s(&stmt);
++                      cb->to = timeout_add_short(0, sql_query_delayed_callback, cb);
++                      DLLIST_PREPEND(&db->query_delayed_list, cb);
++              } else
++                      default_sql_statement_query(stmt, callback, context);
++      } T_END;
+ }
+ 
+ struct sql_result *sql_statement_query_s(struct sql_statement **_stmt)
+ {
+       struct sql_statement *stmt = *_stmt;
++      struct sql_result *result;
+ 
+       *_stmt = NULL;
+-      if (stmt->db->v.statement_query_s != NULL)
+-              return stmt->db->v.statement_query_s(stmt);
+-      else
+-              return default_sql_statement_query_s(stmt);
++      T_BEGIN {
++              if (stmt->db->v.statement_query_s != NULL)
++                      result = stmt->db->v.statement_query_s(stmt);
++              else
++                      result = default_sql_statement_query_s(stmt);
++      } T_END;
++      return result;
+ }
+ 
+ void sql_result_ref(struct sql_result *result)
+@@ -1011,10 +1017,12 @@ void sql_update_stmt(struct sql_transact
+       struct sql_statement *stmt = *_stmt;
+ 
+       *_stmt = NULL;
+-      if (ctx->db->v.update_stmt != NULL)
+-              ctx->db->v.update_stmt(ctx, stmt, NULL);
+-      else
+-              default_sql_update_stmt(ctx, stmt, NULL);
++      T_BEGIN {
++              if (ctx->db->v.update_stmt != NULL)
++                      ctx->db->v.update_stmt(ctx, stmt, NULL);
++              else
++                      default_sql_update_stmt(ctx, stmt, NULL);
++      } T_END;
+ }
+ 
+ void sql_update_get_rows(struct sql_transaction_context *ctx, const char *query,
+@@ -1030,10 +1038,12 @@ void sql_update_stmt_get_rows(struct sql
+       struct sql_statement *stmt = *_stmt;
+ 
+       *_stmt = NULL;
+-      if (ctx->db->v.update_stmt != NULL)
+-              ctx->db->v.update_stmt(ctx, stmt, affected_rows);
+-      else
+-              default_sql_update_stmt(ctx, stmt, affected_rows);
++      T_BEGIN {
++              if (ctx->db->v.update_stmt != NULL)
++                      ctx->db->v.update_stmt(ctx, stmt, affected_rows);
++              else
++                      default_sql_update_stmt(ctx, stmt, affected_rows);
++      } T_END;
+ }
+ 
+ void sql_db_set_state(struct sql_db *db, enum sql_db_state state)
diff --git a/patches/0008-doveadm-Fix-ubsan-unsigned-integer-overflow-error.patch b/patches/0008-doveadm-Fix-ubsan-unsigned-integer-overflow-error.patch
new file mode 100644 (file)
index 0000000..5c1c4d7
--- /dev/null
@@ -0,0 +1,29 @@
+From aba5b8556952dbb47b9ed8b338d170cb73c80ce6 Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Thu, 16 Apr 2026 13:27:42 +0200
+Subject: [PATCH 08/12] doveadm: Fix ubsan unsigned integer overflow error
+
+---
+ src/doveadm/doveadm-who.c | 4 ++--
+ 1 file changed, 2 insertions(+), 2 deletions(-)
+
+diff --git a/src/doveadm/doveadm-who.c b/src/doveadm/doveadm-who.c
+index 2fbef7bb94..ecdbb88bc7 100644
+--- a/src/doveadm/doveadm-who.c
++++ b/src/doveadm/doveadm-who.c
+@@ -49,10 +49,10 @@ static unsigned int who_user_hash(const struct who_user *user)
+       unsigned int hash = str_hash(user->service);
+ 
+       if (user->username[0] != '\0')
+-              hash += str_hash(user->username);
++              hash ^= str_hash(user->username);
+       else {
+               who_user_ip(user, &ip);
+-              hash += net_ip_hash(&ip);
++              hash ^= net_ip_hash(&ip);
+       }
+       return hash;
+ }
+-- 
+2.47.3
+
diff --git a/patches/0008-lib-mail-Make-istream-header-filter-s-per-header-siz.patch b/patches/0008-lib-mail-Make-istream-header-filter-s-per-header-siz.patch
new file mode 100644 (file)
index 0000000..241eb02
--- /dev/null
@@ -0,0 +1,91 @@
+From a444863fef605978c6f18e85db07f382d6f21d23 Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Sun, 19 Apr 2026 18:15:49 +0300
+Subject: [PATCH 08/14] lib-mail: Make istream-header-filter's per-header size
+ limit configurable
+
+Until now the internal message_header_parser_ctx used the default
+MESSAGE_HEADER_BLOCK_DEFAULT_MAX_SIZE (10 MB) cap. In practice that cap
+only bounded hdr->full_value; per-chunk hdr->value was delivered in full
+regardless. A subsequent change to message_parse_header_next() will
+clamp hdr->value cumulatively as well, and at that point the 10 MB
+default would silently truncate data for callers like mbox-save that
+stream raw header bytes to storage.
+
+Default the filter to SIZE_MAX (unlimited) to preserve the effective
+behavior and add i_stream_header_filter_set_max_header_block_size() so
+callers that genuinely want a cap (index_mail_get_header_stream()) can
+opt in.
+
+Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
+---
+ src/lib-mail/istream-header-filter.c | 20 ++++++++++++++++++++
+ src/lib-mail/istream-header-filter.h |  8 ++++++++
+ 2 files changed, 28 insertions(+)
+
+Index: trixie/src/lib-mail/istream-header-filter.c
+===================================================================
+--- trixie.orig/src/lib-mail/istream-header-filter.c
++++ trixie/src/lib-mail/istream-header-filter.c
+@@ -29,6 +29,7 @@ struct header_filter_istream {
+       buffer_t *hdr_buf;
+       int snapshot_pending_refcount;
+       struct message_size header_size;
++      size_t max_header_block_size;
+       uoff_t skip_count;
+       uoff_t last_lf_offset;
+ 
+@@ -194,6 +195,8 @@ static ssize_t read_header(struct header
+               mstream->hdr_ctx =
+                       message_parse_header_init(mstream->istream.parent,
+                                                 NULL, 0);
++              message_parse_header_set_limit(mstream->hdr_ctx,
++                                             mstream->max_header_block_size);
+       }
+ 
+       /* remove skipped data from hdr_buf */
+@@ -709,6 +712,9 @@ i_stream_create_header_filter(struct ist
+       mstream->pool = pool_alloconly_create(MEMPOOL_GROWING
+                                             "header filter stream", 256);
+       mstream->istream.max_buffer_size = input->real_stream->max_buffer_size;
++      /* Default to no per-header block size limit. Callers that want one
++         call i_stream_header_filter_set_max_header_block_size(). */
++      mstream->max_header_block_size = SIZE_MAX;
+ 
+       mstream->headers = headers_count == 0 ? NULL :
+               p_new(mstream->pool, const char *, headers_count);
+@@ -762,3 +768,17 @@ void i_stream_header_filter_add(struct h
+       buffer_append(input->hdr_buf, data, size);
+       input->headers_edited = TRUE;
+ }
++
++void i_stream_header_filter_set_max_header_block_size(
++      struct istream *input, size_t max_header_block_size)
++{
++      struct header_filter_istream *mstream =
++              container_of(input->real_stream, struct header_filter_istream,
++                           istream);
++
++      mstream->max_header_block_size = max_header_block_size;
++      if (mstream->hdr_ctx != NULL) {
++              message_parse_header_set_limit(mstream->hdr_ctx,
++                                             max_header_block_size);
++      }
++}
+Index: trixie/src/lib-mail/istream-header-filter.h
+===================================================================
+--- trixie.orig/src/lib-mail/istream-header-filter.h
++++ trixie/src/lib-mail/istream-header-filter.h
+@@ -49,4 +49,12 @@ i_stream_create_header_filter(struct ist
+ void i_stream_header_filter_add(struct header_filter_istream *input,
+                               const void *data, size_t size);
+ 
++/* Set a per-header-block maximum size for this filter stream. Data beyond
++   this limit in any single header is dropped from the filter's output and
++   from values delivered to the filter callback. The default is SIZE_MAX
++   (unlimited). Use MESSAGE_HEADER_BLOCK_DEFAULT_MAX_SIZE from
++   message-header-parser.h for the standard cap. */
++void i_stream_header_filter_set_max_header_block_size(
++      struct istream *input, size_t max_header_block_size);
++
+ #endif
diff --git a/patches/0008-lib-sieve-Reduce-CPU-limit-check-interval-in-contain.patch b/patches/0008-lib-sieve-Reduce-CPU-limit-check-interval-in-contain.patch
new file mode 100644 (file)
index 0000000..e3cb1c5
--- /dev/null
@@ -0,0 +1,40 @@
+From 01c95a5503ec53c40f12d40de0b508838f6b041d Mon Sep 17 00:00:00 2001
+From: Aki Tuomi <aki.tuomi@open-xchange.com>
+Date: Mon, 25 May 2026 14:54:03 +0000
+Subject: [PATCH] lib-sieve: Reduce CPU limit check interval in :contains and
+ :matches matchers
+
+Reduce the interval to 64 so worst-case work between checks is ~64 MiB.
+cpu_limit_exceeded() is a signal-counter comparison with no syscall, so
+the higher call frequency has no measurable cost.
+---
+ src/lib-sieve/mcht-contains.c | 2 +-
+ src/lib-sieve/mcht-matches.c  | 2 +-
+ 2 files changed, 2 insertions(+), 2 deletions(-)
+
+Index: trixie/pigeonhole/src/lib-sieve/mcht-contains.c
+===================================================================
+--- trixie.orig/pigeonhole/src/lib-sieve/mcht-contains.c
++++ trixie/pigeonhole/src/lib-sieve/mcht-contains.c
+@@ -45,7 +45,7 @@ const struct sieve_match_type_def contai
+  * configured sieve_max_cpu_time (which is otherwise only checked between
+  * bytecode operations).
+  */
+-#define SIEVE_CONTAINS_CPU_CHECK_INTERVAL 4096
++#define SIEVE_CONTAINS_CPU_CHECK_INTERVAL 64
+ 
+ static int mcht_contains_match_key
+ (struct sieve_match_context *mctx, const char *val, size_t val_size,
+Index: trixie/pigeonhole/src/lib-sieve/mcht-matches.c
+===================================================================
+--- trixie.orig/pigeonhole/src/lib-sieve/mcht-matches.c
++++ trixie/pigeonhole/src/lib-sieve/mcht-matches.c
+@@ -53,7 +53,7 @@ const struct sieve_match_type_def matche
+  * configured sieve_max_cpu_time. Returns 1 on match, 0 on exhaustion, or -1
+  * when the CPU time limit was exceeded (mctx->exec_status is set).
+  */
+-#define SIEVE_MATCHES_CPU_CHECK_INTERVAL 4096
++#define SIEVE_MATCHES_CPU_CHECK_INTERVAL 64
+ 
+ static int
+ _string_find(struct sieve_match_context *mctx,
diff --git a/patches/0009-config-Assume-dovecot_config_version-0.0.0-is-the-sa.patch b/patches/0009-config-Assume-dovecot_config_version-0.0.0-is-the-sa.patch
new file mode 100644 (file)
index 0000000..9a07818
--- /dev/null
@@ -0,0 +1,92 @@
+From f5b9e0c2310b6fa744cf098dea9c03f88a985898 Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Mon, 29 Sep 2025 14:09:42 +0300
+Subject: [PATCH 09/12] config: Assume dovecot_config_version=0.0.0 is the same
+ as the latest version
+
+It's used for git builds.
+---
+ src/config/config-parser.c | 16 +++++++++++++++-
+ src/config/config-parser.h |  3 +++
+ src/config/doveconf.c      | 10 +++++++++-
+ 3 files changed, 27 insertions(+), 2 deletions(-)
+
+diff --git a/src/config/config-parser.c b/src/config/config-parser.c
+index b2883538ae..7e9db599f4 100644
+--- a/src/config/config-parser.c
++++ b/src/config/config-parser.c
+@@ -2604,6 +2604,17 @@ static bool config_version_find(const char *version, const char **error_r)
+       return TRUE;
+ }
+ 
++static bool
++dovecot_config_version_equals(struct config_parser_context *ctx, const char *value)
++{
++      if (strcmp(ctx->dovecot_config_version, value) == 0)
++              return TRUE;
++      if (strcmp(ctx->dovecot_config_version, CONFIG_VERSION_MAX) == 0 &&
++          strcmp(value, CONFIG_VERSION_GIT) == 0)
++              return TRUE;
++      return FALSE;
++}
++
+ static bool config_parser_get_version(struct config_parser_context *ctx,
+                                     const struct config_line *line)
+ {
+@@ -2616,7 +2627,7 @@ static bool config_parser_get_version(struct config_parser_context *ctx,
+       if (strcmp(line->key, "dovecot_config_version") == 0) {
+               if (ctx->dovecot_config_version == NULL)
+                       ;
+-              else if (strcmp(ctx->dovecot_config_version, line->value) != 0) {
++              else if (!dovecot_config_version_equals(ctx, line->value)) {
+                       ctx->error = "dovecot_config_version value can't be changed once set";
+                       return TRUE;
+               } else {
+@@ -2636,6 +2647,9 @@ static bool config_parser_get_version(struct config_parser_context *ctx,
+       else if (!config_version_find(line->value, &error)) {
+               ctx->error = p_strdup_printf(ctx->pool,
+                       "Invalid dovecot_config_version: %s", error);
++      } else if (strcmp(line->value, CONFIG_VERSION_GIT) == 0) {
++              /* git build - this is the same as the latest version. */
++              ctx->dovecot_config_version = CONFIG_VERSION_MAX;
+       } else {
+               ctx->dovecot_config_version = p_strdup(ctx->pool, line->value);
+       }
+diff --git a/src/config/config-parser.h b/src/config/config-parser.h
+index 949aac100b..f9d70f1be8 100644
+--- a/src/config/config-parser.h
++++ b/src/config/config-parser.h
+@@ -5,6 +5,9 @@
+ 
+ #define CONFIG_MODULE_DIR MODULEDIR"/settings"
+ 
++#define CONFIG_VERSION_GIT "0.0.0"
++#define CONFIG_VERSION_MAX "9999.9999.9999"
++
+ /* change_counter used for default settings created internally */
+ #define CONFIG_PARSER_CHANGE_DEFAULTS 1
+ /* change_counter used for settings changed by configuration file */
+diff --git a/src/config/doveconf.c b/src/config/doveconf.c
+index 17dafbf006..825d84545c 100644
+--- a/src/config/doveconf.c
++++ b/src/config/doveconf.c
+@@ -1297,7 +1297,15 @@ int main(int argc, char *argv[])
+                               printf("# %u default setting changes since version %s\n",
+                                      count, version);
+                       }
+-                      printf("dovecot_config_version = %s\n", version);
++                      if (strcmp(version, CONFIG_VERSION_MAX) != 0)
++                              printf("dovecot_config_version = %s\n", version);
++                      else {
++                              /* GIT version was changed to MAX for easier
++                                 comparisons internally. However, output it
++                                 back as the original GIT version. */
++                              printf("dovecot_config_version = %s\n",
++                                     CONFIG_VERSION_GIT);
++                      }
+               }
+               if (!config_path_specified)
+                       check_wrong_config(config_path);
+-- 
+2.47.3
+
diff --git a/patches/0009-lib-storage-Cap-per-header-size-in-index_mail_get_he.patch b/patches/0009-lib-storage-Cap-per-header-size-in-index_mail_get_he.patch
new file mode 100644 (file)
index 0000000..4ae0dca
--- /dev/null
@@ -0,0 +1,42 @@
+From 6802995e1302718a81bc1e39bdd43d8296421d11 Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Sun, 19 Apr 2026 18:28:16 +0300
+Subject: [PATCH 09/14] lib-storage: Cap per-header size in
+ index_mail_get_header_stream()
+
+Apply MESSAGE_HEADER_BLOCK_DEFAULT_MAX_SIZE (10 MB) to the header-filter
+istream used for populating the header cache. A pathological single
+header (for example a To: with millions of addresses) otherwise grows
+mail->header_data and the cache write buffer in lockstep with the raw
+header size, which can push the imap process over vsz_limit on FETCH
+ENVELOPE / BODYSTRUCTURE.
+
+On its own this change does not yet bound hdr->value delivery; that
+requires the upcoming change to message_parse_header_next() to clamp
+per-chunk value_len cumulatively. Setting the limit here now lets that
+follow-up take effect without further touching this file.
+
+Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
+---
+ src/lib-storage/index/index-mail-headers.c | 5 +++++
+ 1 file changed, 5 insertions(+)
+
+diff --git a/src/lib-storage/index/index-mail-headers.c b/src/lib-storage/index/index-mail-headers.c
+index 18c922239d..a9f4d55c0d 100644
+--- a/src/lib-storage/index/index-mail-headers.c
++++ b/src/lib-storage/index/index-mail-headers.c
+@@ -992,6 +992,11 @@ int index_mail_get_header_stream(struct mail *_mail,
+                                             HEADER_FILTER_HIDE_BODY,
+                                             headers->name, headers->count,
+                                             header_cache_callback, mail);
++      /* Cap per-header data so a single pathological header cannot exhaust
++         memory in mail->header_data / the filter's buffer. */
++      i_stream_header_filter_set_max_header_block_size(
++              mail->data.filter_stream,
++              MESSAGE_HEADER_BLOCK_DEFAULT_MAX_SIZE);
+       *stream_r = mail->data.filter_stream;
+       return 0;
+ }
+-- 
+2.47.3
+
diff --git a/patches/0010-lib-mail-Clamp-hdr-value-cumulatively-against-header.patch b/patches/0010-lib-mail-Clamp-hdr-value-cumulatively-against-header.patch
new file mode 100644 (file)
index 0000000..18a4332
--- /dev/null
@@ -0,0 +1,123 @@
+From 64ab59e17533a7eb63cbd4cd8cce81a39b32b2b1 Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Mon, 20 Apr 2026 00:16:14 +0300
+Subject: [PATCH 10/14] lib-mail: Clamp hdr->value cumulatively against
+ header_block_max_size
+
+Until now header_block_max_size only bounded hdr->full_value via
+value_buf.  Continued chunks returned to the caller via hdr->value were
+left at the raw chunk size, so a caller that consumed hdr->value per
+chunk without ever requesting use_full_value (e.g. the header-cache
+path in index_mail_parse_header()) could accumulate the full raw header
+size.  A pathological To: with millions of addresses could grow
+mail->header_data and the cache write buffer to tens of megabytes each,
+driving the imap process over vsz_limit on FETCH ENVELOPE.
+
+Reinterpret header_block_total_size as the running sum of line_value_size
+across all chunks of all headers, and clamp each new chunk against the
+remaining header_block_max_size budget. Propagate the clamped size to
+line->value_len in the two continued-line branches that previously left
+it untouched. value_buf is bounded implicitly since every append uses
+line_value_size. The up-front per-chunk clamp
+(line->value_len = MIN(value_len, max_size)) is now subsumed by the
+cumulative clamp and has been removed.
+
+Update the truncation tests that were documenting the old
+"value_len stays at raw chunk size" behavior.
+
+Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
+---
+ src/lib-mail/message-header-parser.c      | 16 +++++++++++-----
+ src/lib-mail/test-message-header-parser.c | 16 ++++++++++++----
+ 2 files changed, 23 insertions(+), 9 deletions(-)
+
+diff --git a/src/lib-mail/message-header-parser.c b/src/lib-mail/message-header-parser.c
+index b240a47884..3026a35f29 100644
+--- a/src/lib-mail/message-header-parser.c
++++ b/src/lib-mail/message-header-parser.c
+@@ -96,7 +96,6 @@ int message_parse_header_next(struct message_header_parser_ctx *ctx,
+               /* new header line */
+               line->name_offset = ctx->input->v_offset;
+               colon_pos = UINT_MAX;
+-              ctx->header_block_total_size += ctx->value_buf->used;
+               buffer_set_used_size(ctx->value_buf, 0);
+       }
+ 
+@@ -362,12 +361,17 @@ int message_parse_header_next(struct message_header_parser_ctx *ctx,
+               }
+       }
+ 
+-      line->value_len = I_MIN(line->value_len, ctx->header_block_max_size);
+       size_t line_value_size = line->value_len;
+-      size_t header_total_used = ctx->header_block_total_size + ctx->value_buf->used;
+-      size_t line_available = ctx->header_block_max_size <= header_total_used ? 0 :
+-                              ctx->header_block_max_size - header_total_used;
++      /* Clamp line_value_size against the remaining header_block_max_size
++         budget. header_block_total_size is the running sum of
++         line_value_size across all chunks of all headers; value_buf growth
++         is bounded implicitly because every append to value_buf uses
++         line_value_size. */
++      size_t line_available =
++              ctx->header_block_max_size <= ctx->header_block_total_size ? 0 :
++              ctx->header_block_max_size - ctx->header_block_total_size;
+       line_value_size = I_MIN(line_value_size, line_available);
++      ctx->header_block_total_size += line_value_size;
+ 
+       if (!line->continued) {
+               /* first header line. make a copy of the line since we can't
+@@ -397,10 +401,12 @@ int message_parse_header_next(struct message_header_parser_ctx *ctx,
+ 
+               line->full_value = ctx->value_buf->data;
+               line->full_value_len = ctx->value_buf->used;
++              line->value_len = line_value_size;
+       } else {
+               /* we didn't want full_value, and this is a continued line. */
+               line->full_value = NULL;
+               line->full_value_len = 0;
++              line->value_len = line_value_size;
+       }
+ 
+       /* always reset it */
+diff --git a/src/lib-mail/test-message-header-parser.c b/src/lib-mail/test-message-header-parser.c
+index 5395c54ed6..f2c9184e66 100644
+--- a/src/lib-mail/test-message-header-parser.c
++++ b/src/lib-mail/test-message-header-parser.c
+@@ -494,11 +494,14 @@ static void test_message_header_truncation_clean_oneline(void)
+       struct message_header_parser_ctx *parser = message_parse_header_init(input, NULL, MESSAGE_HEADER_PARSER_FLAG_CLEAN_ONELINE);
+       message_parse_header_set_limit(parser, 96);
+ 
++      /* hdr->value is now also clamped cumulatively against the same
++         header_block_max_size budget as full_value, so later chunks of a
++         multiline header shrink or disappear once the budget is used up. */
+       assert_parse_line( 1, "header1", "this is short",                            "this is short");
+       assert_parse_line( 2, "header2", "this is multiline",                        "this is multiline");
+       assert_parse_line( 3, "header2", " and long 343638404244464850525456586062", "this is multiline and long 343638404244464850525456586062");
+-      assert_parse_line( 4, "header2", " 64666870727476788082848688909294969800",  "this is multiline and long 343638404244464850525456586062 6466687072747678808284868");
+-      assert_parse_line( 5, "header2", " 02040608101214161820222426283032343638",  "this is multiline and long 343638404244464850525456586062 6466687072747678808284868");
++      assert_parse_line( 4, "header2", " 6466687072747678808284868",               "this is multiline and long 343638404244464850525456586062 6466687072747678808284868");
++      assert_parse_line( 5, "header2", "",                                         "this is multiline and long 343638404244464850525456586062 6466687072747678808284868");
+       assert_parse_line( 6, "header3", "", "");
+       test_assert(message_parse_header_next(parser, &hdr) > 0 && hdr->eoh);
+ 
+@@ -515,11 +518,16 @@ static void test_message_header_truncation_flag0(void)
+       struct message_header_parser_ctx *parser = message_parse_header_init(input, NULL, 0);
+       message_parse_header_set_limit(parser, 96);
+ 
++      /* hdr->value is clamped cumulatively against header_block_max_size.
++         The \n that NO flags inserts between continuations goes into
++         value_buf but is not added to header_block_total_size, so line 4's
++         value hits the same 26-byte cap as in CLEAN_ONELINE and
++         full_value_len is one byte larger than the nominal limit. */
+       assert_parse_line( 1, "header1", "this is short",                            "this is short");
+       assert_parse_line( 2, "header2", "this is multiline",                        "this is multiline");
+       assert_parse_line( 3, "header2", " and long 343638404244464850525456586062", "this is multiline\n and long 343638404244464850525456586062");
+-      assert_parse_line( 4, "header2", " 64666870727476788082848688909294969800",  "this is multiline\n and long 343638404244464850525456586062\n 646668707274767880828486");
+-      assert_parse_line( 5, "header2", " 02040608101214161820222426283032343638",  "this is multiline\n and long 343638404244464850525456586062\n 646668707274767880828486");
++      assert_parse_line( 4, "header2", " 6466687072747678808284868",               "this is multiline\n and long 343638404244464850525456586062\n 6466687072747678808284868");
++      assert_parse_line( 5, "header2", "",                                         "this is multiline\n and long 343638404244464850525456586062\n 6466687072747678808284868");
+       assert_parse_line( 6, "header3", "", "");
+       test_assert(message_parse_header_next(parser, &hdr) > 0 && hdr->eoh);
+ 
+-- 
+2.47.3
+
diff --git a/patches/0010-lib-master-Move-config_version_find-from-src-config-.patch b/patches/0010-lib-master-Move-config_version_find-from-src-config-.patch
new file mode 100644 (file)
index 0000000..b11074d
--- /dev/null
@@ -0,0 +1,123 @@
+From 9d9efd5ea41e0e3183e1e53d4326f40b65dd9ef8 Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Fri, 1 May 2026 16:56:21 +0000
+Subject: [PATCH 10/12] lib-master: Move config_version_find() from
+ src/config/config-parser.c
+
+Rename to dovecot_config_version_find() and expose it via
+master-service-settings.h so non-config callers (e.g. the upcoming
+dovecot_storage_version validation in master_service_settings_check())
+can share the same supported-versions list. No behaviour change.
+---
+ src/config/config-parser.c               | 31 ++++++-------------
+ src/lib-master/master-service-settings.c | 38 ++++++++++++++++++++++++
+ src/lib-master/master-service-settings.h |  5 ++++
+ 3 files changed, 52 insertions(+), 22 deletions(-)
+
+Index: trixie/src/config/config-parser.c
+===================================================================
+--- trixie.orig/src/config/config-parser.c
++++ trixie/src/config/config-parser.c
+@@ -2580,30 +2580,6 @@ config_parser_check_warnings(struct conf
+       hash_table_insert(ctx->seen_settings, path, first_pos);
+ }
+ 
+-static bool config_version_find(const char *version, const char **error_r)
+-{
+-      const char *const supported_versions[] = {
+-#ifdef DOVECOT_PRO_EDITION
+-              "3.1.0",
+-#else
+-              "2.4.0",
+-#endif
+-              NULL
+-      };
+-      /* FIXME: implement full version checking later */
+-      if (!str_array_find(supported_versions, version) &&
+-          strcmp(DOVECOT_CONFIG_VERSION, version) != 0) {
+-              *error_r = t_strdup_printf(
+-                      "Currently supported versions are: %s%s",
+-                      t_strarray_join(supported_versions, " "),
+-                      str_array_find(supported_versions,
+-                                     DOVECOT_CONFIG_VERSION) ? "" :
+-                      t_strdup_printf(" %s", DOVECOT_CONFIG_VERSION));
+-              return FALSE;
+-      }
+-      return TRUE;
+-}
+-
+ static bool
+ dovecot_config_version_equals(struct config_parser_context *ctx, const char *value)
+ {
+@@ -2644,7 +2620,7 @@ static bool config_parser_get_version(st
+ 
+       if (line->type != CONFIG_LINE_TYPE_KEYVALUE)
+               ctx->error = "Invalid dovecot_config_version: value is not a string";
+-      else if (!config_version_find(line->value, &error)) {
++      else if (!dovecot_config_version_find(line->value, &error)) {
+               ctx->error = p_strdup_printf(ctx->pool,
+                       "Invalid dovecot_config_version: %s", error);
+       } else if (strcmp(line->value, CONFIG_VERSION_GIT) == 0) {
+Index: trixie/src/lib-master/master-service-settings.c
+===================================================================
+--- trixie.orig/src/lib-master/master-service-settings.c
++++ trixie/src/lib-master/master-service-settings.c
+@@ -146,6 +146,44 @@ master_service_set_process_shutdown_filt
+       master_service_set_process_shutdown_filter(master_service, filter);
+ }
+ 
++/* List of dovecot_config_versions accepted at parse / startup time.
++   dovecot_storage_version accepts these as well, plus an extra legacy set
++   defined in storage_version_check(). */
++static const char *const dovecot_config_supported_versions[] = {
++#ifdef DOVECOT_PRO_EDITION
++      "3.1.0",
++      "3.1.1",
++      "3.1.2",
++      "3.1.3",
++      "3.1.4",
++      "3.1.5",
++      "3.2.0",
++#else
++      "2.4.0",
++      "2.4.1",
++      "2.4.2",
++      "2.4.3",
++      "2.4.4",
++#endif
++      NULL
++};
++
++bool dovecot_config_version_find(const char *version, const char **error_r)
++{
++      /* FIXME: implement full version checking later */
++      if (!str_array_find(dovecot_config_supported_versions, version) &&
++          strcmp(DOVECOT_CONFIG_VERSION, version) != 0) {
++              *error_r = t_strdup_printf(
++                      "Currently supported versions are: %s%s",
++                      t_strarray_join(dovecot_config_supported_versions, " "),
++                      str_array_find(dovecot_config_supported_versions,
++                                     DOVECOT_CONFIG_VERSION) ? "" :
++                      t_strdup_printf(" %s", DOVECOT_CONFIG_VERSION));
++              return FALSE;
++      }
++      return TRUE;
++}
++
+ static bool storage_version_check(const char *version, const char **error_r)
+ {
+ #define STORAGE_MIN_VERSION "2.3.0"
+Index: trixie/src/lib-master/master-service-settings.h
+===================================================================
+--- trixie.orig/src/lib-master/master-service-settings.h
++++ trixie/src/lib-master/master-service-settings.h
+@@ -95,4 +95,9 @@ master_service_get_service_settings(stru
+ const char *
+ master_service_get_import_environment_keyvals(struct master_service *service);
+ 
++/* Returns TRUE if version is in the list of dovecot_config_version values
++   accepted by this build. Sets *error_r to a descriptive list of supported
++   versions on FALSE. */
++bool dovecot_config_version_find(const char *version, const char **error_r);
++
+ #endif
diff --git a/patches/0011-lib-lib-master-Move-version_-to-lib-version.-ch.patch b/patches/0011-lib-lib-master-Move-version_-to-lib-version.-ch.patch
new file mode 100644 (file)
index 0000000..8422135
--- /dev/null
@@ -0,0 +1,591 @@
+From 0e7ba48c290b251b2f264192dd559b72d48a8005 Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Sun, 5 Oct 2025 18:49:13 +0300
+Subject: [PATCH 11/12] lib, lib-master: Move version_*() to lib/version.[ch]
+
+---
+ src/anvil/anvil-connection.c                  |  1 +
+ src/config/config-connection.c                |  1 +
+ src/config/config-parser.c                    |  3 +-
+ src/config/old-set-parser.c                   |  2 +-
+ src/doveadm/client-connection-tcp.c           |  2 +-
+ src/doveadm/doveadm-penalty.c                 |  2 +-
+ src/doveadm/doveadm-stats.c                   |  2 +-
+ src/doveadm/doveadm-util.c                    |  1 +
+ src/doveadm/doveadm-who.c                     |  2 +-
+ src/doveadm/dsync/dsync-ibc-stream.c          |  2 +-
+ src/imap-urlauth-login/imap-urlauth-login.c   |  2 +-
+ src/imap/imap-client-hibernate.c              |  2 +-
+ src/lib-doveadm/doveadm-client.c              |  2 +-
+ src/lib-master/Makefile.am                    |  5 --
+ src/lib-master/master-service-settings.c      |  1 +
+ src/lib-master/master-service.c               | 85 ------------------
+ src/lib-master/master-service.h               | 15 ----
+ src/lib/Makefile.am                           |  3 +
+ src/lib/test-lib.inc                          |  1 +
+ .../test-version.c}                           | 16 ++--
+ src/lib/version.c                             | 90 +++++++++++++++++++
+ src/lib/version.h                             | 19 ++++
+ src/util/script-login.c                       |  1 +
+ src/util/script.c                             |  1 +
+ 24 files changed, 136 insertions(+), 125 deletions(-)
+ rename src/{lib-master/test-master-service.c => lib/test-version.c} (83%)
+ create mode 100644 src/lib/version.c
+ create mode 100644 src/lib/version.h
+
+diff --git a/src/anvil/anvil-connection.c b/src/anvil/anvil-connection.c
+index 13c57bf3e7..9cd44563ed 100644
+--- a/src/anvil/anvil-connection.c
++++ b/src/anvil/anvil-connection.c
+@@ -11,6 +11,7 @@
+ #include "connection.h"
+ #include "str.h"
+ #include "strescape.h"
++#include "version.h"
+ #include "master-service.h"
+ #include "master-interface.h"
+ #include "connect-limit.h"
+diff --git a/src/config/config-connection.c b/src/config/config-connection.c
+index d9e0a13207..265c702601 100644
+--- a/src/config/config-connection.c
++++ b/src/config/config-connection.c
+@@ -7,6 +7,7 @@
+ #include "ostream.h"
+ #include "ostream-unix.h"
+ #include "strescape.h"
++#include "version.h"
+ #include "settings-parser.h"
+ #include "master-service.h"
+ #include "master-service-settings.h"
+diff --git a/src/config/config-parser.c b/src/config/config-parser.c
+index 55e124e88b..07dca715f9 100644
+--- a/src/config/config-parser.c
++++ b/src/config/config-parser.c
+@@ -9,9 +9,9 @@
+ #include "strescape.h"
+ #include "istream.h"
+ #include "module-dir.h"
++#include "version.h"
+ #include "settings.h"
+ #include "service-settings.h"
+-#include "master-service.h"
+ #include "master-service-settings.h"
+ #include "all-settings.h"
+ #include "old-set-parser.h"
+@@ -22,6 +22,7 @@
+ 
+ #include "default-settings-import.h"
+ 
++#include <stdio.h>
+ #include <unistd.h>
+ #include <fcntl.h>
+ #include <time.h>
+diff --git a/src/config/old-set-parser.c b/src/config/old-set-parser.c
+index 4ae8506dc7..ee2cb513c9 100644
+--- a/src/config/old-set-parser.c
++++ b/src/config/old-set-parser.c
+@@ -2,7 +2,7 @@
+ 
+ #include "lib.h"
+ #include "array.h"
+-#include "master-service.h"
++#include "version.h"
+ #include "settings-history.h"
+ #include "config-parser-private.h"
+ #include "old-set-parser.h"
+diff --git a/src/doveadm/client-connection-tcp.c b/src/doveadm/client-connection-tcp.c
+index 9299596f29..936bb03f01 100644
+--- a/src/doveadm/client-connection-tcp.c
++++ b/src/doveadm/client-connection-tcp.c
+@@ -8,9 +8,9 @@
+ #include "istream.h"
+ #include "ostream.h"
+ #include "strescape.h"
++#include "version.h"
+ #include "iostream-ssl.h"
+ #include "ostream-multiplex.h"
+-#include "master-service.h"
+ #include "master-service-ssl.h"
+ #include "mail-storage-service.h"
+ #include "doveadm-util.h"
+diff --git a/src/doveadm/doveadm-penalty.c b/src/doveadm/doveadm-penalty.c
+index 8a098b3453..71a997e2e8 100644
+--- a/src/doveadm/doveadm-penalty.c
++++ b/src/doveadm/doveadm-penalty.c
+@@ -7,7 +7,7 @@
+ #include "hash.h"
+ #include "strescape.h"
+ #include "time-util.h"
+-#include "master-service.h"
++#include "version.h"
+ #include "doveadm.h"
+ #include "doveadm-print.h"
+ 
+diff --git a/src/doveadm/doveadm-stats.c b/src/doveadm/doveadm-stats.c
+index bd7552211a..87aef26a89 100644
+--- a/src/doveadm/doveadm-stats.c
++++ b/src/doveadm/doveadm-stats.c
+@@ -6,7 +6,7 @@
+ #include "str.h"
+ #include "strescape.h"
+ #include "write-full.h"
+-#include "master-service.h"
++#include "version.h"
+ #include "doveadm.h"
+ #include "doveadm-print.h"
+ #include "stats-settings.h"
+diff --git a/src/doveadm/doveadm-util.c b/src/doveadm/doveadm-util.c
+index c21dbee6c0..4bea2437cc 100644
+--- a/src/doveadm/doveadm-util.c
++++ b/src/doveadm/doveadm-util.c
+@@ -7,6 +7,7 @@
+ #include "ostream.h"
+ #include "net.h"
+ #include "time-util.h"
++#include "version.h"
+ #include "master-service.h"
+ #include "module-dir.h"
+ #include "doveadm-settings.h"
+diff --git a/src/doveadm/doveadm-who.c b/src/doveadm/doveadm-who.c
+index ecdbb88bc7..29172afba0 100644
+--- a/src/doveadm/doveadm-who.c
++++ b/src/doveadm/doveadm-who.c
+@@ -8,7 +8,7 @@
+ #include "hash.h"
+ #include "str.h"
+ #include "strescape.h"
+-#include "master-service.h"
++#include "version.h"
+ #include "doveadm.h"
+ #include "doveadm-print.h"
+ #include "doveadm-who.h"
+diff --git a/src/doveadm/dsync/dsync-ibc-stream.c b/src/doveadm/dsync/dsync-ibc-stream.c
+index 957c569cee..472d1fc517 100644
+--- a/src/doveadm/dsync/dsync-ibc-stream.c
++++ b/src/doveadm/dsync/dsync-ibc-stream.c
+@@ -10,7 +10,7 @@
+ #include "ostream.h"
+ #include "str.h"
+ #include "strescape.h"
+-#include "master-service.h"
++#include "version.h"
+ #include "mail-cache.h"
+ #include "mail-storage-private.h"
+ #include "dsync-serializer.h"
+diff --git a/src/imap-urlauth-login/imap-urlauth-login.c b/src/imap-urlauth-login/imap-urlauth-login.c
+index 23a76f6c81..68b4aab159 100644
+--- a/src/imap-urlauth-login/imap-urlauth-login.c
++++ b/src/imap-urlauth-login/imap-urlauth-login.c
+@@ -8,7 +8,7 @@
+ #include "ioloop.h"
+ #include "istream.h"
+ #include "ostream.h"
+-#include "master-service.h"
++#include "version.h"
+ #include "auth-client.h"
+ #include "client-common.h"
+ 
+diff --git a/src/imap/imap-client-hibernate.c b/src/imap/imap-client-hibernate.c
+index ade5d250ea..067b984cc0 100644
+--- a/src/imap/imap-client-hibernate.c
++++ b/src/imap/imap-client-hibernate.c
+@@ -9,7 +9,7 @@
+ #include "base64.h"
+ #include "str.h"
+ #include "strescape.h"
+-#include "master-service.h"
++#include "version.h"
+ #include "compression.h"
+ #include "mailbox-watch.h"
+ #include "imap-state.h"
+diff --git a/src/lib-doveadm/doveadm-client.c b/src/lib-doveadm/doveadm-client.c
+index 47b09f677f..6d570169ec 100644
+--- a/src/lib-doveadm/doveadm-client.c
++++ b/src/lib-doveadm/doveadm-client.c
+@@ -10,8 +10,8 @@
+ #include "ostream-dot.h"
+ #include "str.h"
+ #include "strescape.h"
++#include "version.h"
+ #include "iostream-ssl.h"
+-#include "master-service.h"
+ #include "doveadm-protocol.h"
+ #include "doveadm-client.h"
+ #include "dns-lookup.h"
+diff --git a/src/lib-master/Makefile.am b/src/lib-master/Makefile.am
+index d6fe251e43..8939f759de 100644
+--- a/src/lib-master/Makefile.am
++++ b/src/lib-master/Makefile.am
+@@ -47,7 +47,6 @@ pkginc_lib_HEADERS = $(headers)
+ 
+ test_programs = \
+       test-event-stats \
+-      test-master-service \
+       test-master-service-settings
+ 
+ noinst_PROGRAMS = $(test_programs)
+@@ -71,10 +70,6 @@ test_event_stats_SOURCES = test-event-stats.c
+ test_event_stats_LDADD = $(test_libs)
+ test_event_stats_DEPENDENCIES = $(test_deps)
+ 
+-test_master_service_SOURCES = test-master-service.c
+-test_master_service_LDADD = $(test_libs)
+-test_master_service_DEPENDENCIES = $(test_deps)
+-
+ test_master_service_settings_SOURCES = test-master-service-settings.c
+ test_master_service_settings_LDADD = $(test_libs)
+ test_master_service_settings_DEPENDENCIES = $(test_deps)
+diff --git a/src/lib-master/master-service-settings.c b/src/lib-master/master-service-settings.c
+index 71438c0c4f..4194c19fab 100644
+--- a/src/lib-master/master-service-settings.c
++++ b/src/lib-master/master-service-settings.c
+@@ -11,6 +11,7 @@
+ #include "eacces-error.h"
+ #include "env-util.h"
+ #include "execv-const.h"
++#include "version.h"
+ #include "settings.h"
+ #include "stats-client.h"
+ #include "master-service-private.h"
+diff --git a/src/lib-master/master-service.c b/src/lib-master/master-service.c
+index a18cb39dbe..8f2ccacee1 100644
+--- a/src/lib-master/master-service.c
++++ b/src/lib-master/master-service.c
+@@ -2035,91 +2035,6 @@ void master_status_update(struct master_service *service)
+       master_status_send(service, important_update);
+ }
+ 
+-bool version_string_verify(const char *line, const char *service_name,
+-                         unsigned int major_version)
+-{
+-      unsigned int minor_version;
+-
+-      return version_string_verify_full(line, service_name,
+-                                        major_version, &minor_version);
+-}
+-
+-bool version_string_verify_full(const char *line, const char *service_name,
+-                              unsigned int major_version,
+-                              unsigned int *minor_version_r)
+-{
+-      size_t service_name_len = strlen(service_name);
+-      bool ret;
+-
+-      if (!str_begins(line, "VERSION\t", &line))
+-              return FALSE;
+-
+-      if (strncmp(line, service_name, service_name_len) != 0 ||
+-          line[service_name_len] != '\t')
+-              return FALSE;
+-      line += service_name_len + 1;
+-
+-      T_BEGIN {
+-              const char *p = strchr(line, '\t');
+-
+-              if (p == NULL)
+-                      ret = FALSE;
+-              else {
+-                      ret = str_uint_equals(t_strdup_until(line, p),
+-                                            major_version);
+-                      if (str_to_uint(p+1, minor_version_r) < 0)
+-                              ret = FALSE;
+-              }
+-      } T_END;
+-      return ret;
+-}
+-
+-int version_cmp(const char *version1, const char *version2)
+-{
+-      unsigned int v1, v2;
+-
+-      do {
+-              if (str_parse_uint(version1, &v1, &version1) < 0)
+-                      i_unreached();
+-              if (str_parse_uint(version2, &v2, &version2) < 0)
+-                      i_unreached();
+-              if (*version1 == '.')
+-                      version1++;
+-              else
+-                      i_assert(*version1 == '\0');
+-              if (*version2 == '.')
+-                      version2++;
+-              else
+-                      i_assert(*version2 == '\0');
+-
+-              if (v1 < v2)
+-                      return -1;
+-              if (v1 > v2)
+-                      return 1;
+-      } while (*version1 != '\0' && *version2 != '\0');
+-
+-      if (*version1 != '\0')
+-              return 1;
+-      if (*version2 != '\0')
+-              return -1;
+-      return 0;
+-}
+-
+-bool version_is_valid(const char *version)
+-{
+-      unsigned int i;
+-
+-      for (i = 0; version[i] != '\0'; i++) {
+-              if (version[i] == '.') {
+-                      if (i == 0 || version[i-1] == '.' ||
+-                          version[i+1] == '\0')
+-                              return FALSE;
+-              } else if (version[i] < '0' || version[i] > '9')
+-                      return FALSE;
+-      }
+-      return i > 0;
+-}
+-
+ void master_service_set_process_shutdown_filter(struct master_service *service,
+                                               struct event_filter *filter)
+ {
+diff --git a/src/lib-master/master-service.h b/src/lib-master/master-service.h
+index 1dca3a6403..6c13ddaca7 100644
+--- a/src/lib-master/master-service.h
++++ b/src/lib-master/master-service.h
+@@ -308,21 +308,6 @@ void master_service_deinit(struct master_service **service);
+  */
+ void master_service_deinit_forked(struct master_service **_service);
+ 
+-/* Returns TRUE if line contains compatible service name and major version.
+-   The line is expected to be in format:
+-   VERSION <tab> service_name <tab> major version <tab> minor version */
+-bool version_string_verify(const char *line, const char *service_name,
+-                         unsigned int major_version);
+-/* Same as version_string_verify(), but return the minor version. */
+-bool version_string_verify_full(const char *line, const char *service_name,
+-                              unsigned int major_version,
+-                              unsigned int *minor_version_r);
+-/* Compare number[.number[...]] style version numbers. Assert-crash if the
+-   version strings are invalid. */
+-int version_cmp(const char *version1, const char *version2);
+-/* Returns TRUE if version string is a valid number[.number[...]] string. */
+-bool version_is_valid(const char *version);
+-
+ /* Sets process shutdown filter */
+ void master_service_set_process_shutdown_filter(struct master_service *service,
+                                               struct event_filter *filter);
+diff --git a/src/lib/Makefile.am b/src/lib/Makefile.am
+index eb9b9ddda3..45a4f1160e 100644
+--- a/src/lib/Makefile.am
++++ b/src/lib/Makefile.am
+@@ -197,6 +197,7 @@ liblib_la_SOURCES = \
+       uri-util.c \
+       utc-offset.c \
+       utc-mktime.c \
++      version.c \
+       wildcard-match.c \
+       write-full.c
+ 
+@@ -358,6 +359,7 @@ headers = \
+       uri-util.h \
+       utc-offset.h \
+       utc-mktime.h \
++      version.h \
+       wildcard-match.h \
+       write-full.h
+ 
+@@ -464,6 +466,7 @@ test_lib_SOURCES = \
+       test-unichar.c \
+       test-utc-mktime.c \
+       test-uri.c \
++      test-version.c \
+       test-wildcard-match.c
+ 
+ test_headers = \
+diff --git a/src/lib/test-lib.inc b/src/lib/test-lib.inc
+index 2fceca2f9b..52259e4b96 100644
+--- a/src/lib/test-lib.inc
++++ b/src/lib/test-lib.inc
+@@ -109,4 +109,5 @@ TEST(test_time_util)
+ TEST(test_unichar)
+ TEST(test_uri)
+ TEST(test_utc_mktime)
++TEST(test_version)
+ TEST(test_wildcard_match)
+diff --git a/src/lib-master/test-master-service.c b/src/lib/test-version.c
+similarity index 83%
+rename from src/lib-master/test-master-service.c
+rename to src/lib/test-version.c
+index f30b250628..e938333f08 100644
+--- a/src/lib-master/test-master-service.c
++++ b/src/lib/test-version.c
+@@ -1,7 +1,7 @@
+-/* Copyright (c) 2023 Dovecot authors, see the included COPYING file */
++/* Copyright (c) 2023-2025 Dovecot authors, see the included COPYING file */
+ 
+-#include "lib.h"
+-#include "master-service.h"
++#include "test-lib.h"
++#include "version.h"
+ #include "test-common.h"
+ 
+ static void test_version_is_valid(void)
+@@ -56,12 +56,8 @@ static void test_version_cmp(void)
+       test_end();
+ }
+ 
+-int main(void)
++void test_version(void)
+ {
+-      static void (*const test_functions[])(void) = {
+-              test_version_is_valid,
+-              test_version_cmp,
+-              NULL
+-      };
+-      return test_run(test_functions);
++      test_version_is_valid();
++      test_version_cmp();
+ }
+diff --git a/src/lib/version.c b/src/lib/version.c
+new file mode 100644
+index 0000000000..f8bec420ab
+--- /dev/null
++++ b/src/lib/version.c
+@@ -0,0 +1,90 @@
++/* Copyright (c) 2005-2025 Dovecot authors, see the included COPYING file */
++
++#include "lib.h"
++#include "version.h"
++
++bool version_string_verify(const char *line, const char *service_name,
++                         unsigned int major_version)
++{
++      unsigned int minor_version;
++
++      return version_string_verify_full(line, service_name,
++                                        major_version, &minor_version);
++}
++
++bool version_string_verify_full(const char *line, const char *service_name,
++                              unsigned int major_version,
++                              unsigned int *minor_version_r)
++{
++      size_t service_name_len = strlen(service_name);
++      bool ret;
++
++      if (!str_begins(line, "VERSION\t", &line))
++              return FALSE;
++
++      if (strncmp(line, service_name, service_name_len) != 0 ||
++          line[service_name_len] != '\t')
++              return FALSE;
++      line += service_name_len + 1;
++
++      T_BEGIN {
++              const char *p = strchr(line, '\t');
++
++              if (p == NULL)
++                      ret = FALSE;
++              else {
++                      ret = str_uint_equals(t_strdup_until(line, p),
++                                            major_version);
++                      if (str_to_uint(p+1, minor_version_r) < 0)
++                              ret = FALSE;
++              }
++      } T_END;
++      return ret;
++}
++
++int version_cmp(const char *version1, const char *version2)
++{
++      unsigned int v1, v2;
++
++      do {
++              if (str_parse_uint(version1, &v1, &version1) < 0)
++                      i_unreached();
++              if (str_parse_uint(version2, &v2, &version2) < 0)
++                      i_unreached();
++              if (*version1 == '.')
++                      version1++;
++              else
++                      i_assert(*version1 == '\0');
++              if (*version2 == '.')
++                      version2++;
++              else
++                      i_assert(*version2 == '\0');
++
++              if (v1 < v2)
++                      return -1;
++              if (v1 > v2)
++                      return 1;
++      } while (*version1 != '\0' && *version2 != '\0');
++
++      if (*version1 != '\0')
++              return 1;
++      if (*version2 != '\0')
++              return -1;
++      return 0;
++}
++
++bool version_is_valid(const char *version)
++{
++      unsigned int i;
++
++      for (i = 0; version[i] != '\0'; i++) {
++              if (version[i] == '.') {
++                      if (i == 0 || version[i-1] == '.' ||
++                          version[i+1] == '\0')
++                              return FALSE;
++              } else if (version[i] < '0' || version[i] > '9')
++                      return FALSE;
++      }
++      return i > 0;
++}
++
+diff --git a/src/lib/version.h b/src/lib/version.h
+new file mode 100644
+index 0000000000..f305f7f4e1
+--- /dev/null
++++ b/src/lib/version.h
+@@ -0,0 +1,19 @@
++#ifndef VERSION_H
++#define VERSION_H
++
++/* Returns TRUE if line contains compatible service name and major version.
++   The line is expected to be in format:
++   VERSION <tab> service_name <tab> major version <tab> minor version */
++bool version_string_verify(const char *line, const char *service_name,
++                         unsigned int major_version);
++/* Same as version_string_verify(), but return the minor version. */
++bool version_string_verify_full(const char *line, const char *service_name,
++                              unsigned int major_version,
++                              unsigned int *minor_version_r);
++/* Compare number[.number[...]] style version numbers. Assert-crash if the
++   version strings are invalid. */
++int version_cmp(const char *version1, const char *version2);
++/* Returns TRUE if version string is a valid number[.number[...]] string. */
++bool version_is_valid(const char *version);
++
++#endif
+diff --git a/src/util/script-login.c b/src/util/script-login.c
+index ed356747dc..4df7f23174 100644
+--- a/src/util/script-login.c
++++ b/src/util/script-login.c
+@@ -7,6 +7,7 @@
+ #include "restrict-access.h"
+ #include "str.h"
+ #include "strescape.h"
++#include "version.h"
+ #include "settings-parser.h"
+ #include "mail-storage-service.h"
+ #include "master-interface.h"
+diff --git a/src/util/script.c b/src/util/script.c
+index 5838cdad31..391b87bc06 100644
+--- a/src/util/script.c
++++ b/src/util/script.c
+@@ -8,6 +8,7 @@
+ #include "execv-const.h"
+ #include "write-full.h"
+ #include "restrict-access.h"
++#include "version.h"
+ #include "master-interface.h"
+ #include "master-service.h"
+ 
+-- 
+2.47.3
+
diff --git a/patches/0011-lib-mail-Introduce-struct-message_part_data_limits-a.patch b/patches/0011-lib-mail-Introduce-struct-message_part_data_limits-a.patch
new file mode 100644 (file)
index 0000000..f5da104
--- /dev/null
@@ -0,0 +1,323 @@
+From 3e8ffe61ee5719c05e68d9be7f913dc3cd2a0e6c Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Fri, 17 Apr 2026 15:54:48 +0200
+Subject: [PATCH 11/14] lib-mail: Introduce struct message_part_data_limits and
+ thread it through parsers
+
+Add an empty struct message_part_data_limits and pass it by pointer through
+message_part_data_parse_from_header() and message_part_envelope_parse_from_header().
+
+No behaviour change: the struct has no fields yet and no limits are applied.
+Subsequent commits add the individual limit fields and enforcement.
+
+Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
+---
+ src/lib-imap/test-imap-bodystructure.c     |  3 ++-
+ src/lib-imap/test-imap-envelope.c          |  4 +++-
+ src/lib-mail/message-part-data.c           |  5 ++++-
+ src/lib-mail/message-part-data.h           |  7 +++++++
+ src/lib-mail/test-message-decoder.c        | 16 ++++++++++++----
+ src/lib-mail/test-message-parser.c         |  3 ++-
+ src/lib-mail/test-message-part-data.c      |  3 ++-
+ src/lib-storage/index/index-mail-headers.c | 10 +++++++---
+ src/lib-storage/index/index-mail.c         | 21 +++++++++++++++++----
+ src/lib-storage/index/index-mail.h         |  2 ++
+ 10 files changed, 58 insertions(+), 16 deletions(-)
+
+diff --git a/src/lib-imap/test-imap-bodystructure.c b/src/lib-imap/test-imap-bodystructure.c
+index 40c8a8c6ec..abe35ac6f3 100644
+--- a/src/lib-imap/test-imap-bodystructure.c
++++ b/src/lib-imap/test-imap-bodystructure.c
+@@ -398,10 +398,11 @@ msg_parse(pool_t pool, const char *message, unsigned int max_nested_mime_parts,
+ 
+       input = i_stream_create_from_data(message, strlen(message));
+       parser = message_parser_init(pool, input, &parser_set);
++      struct message_part_data_limits limits = MESSAGE_PART_DATA_LIMITS_INIT;
+       while ((ret = message_parser_parse_next_block(parser, &block)) > 0) {
+               if (parse_bodystructure) {
+                       message_part_data_parse_from_header(pool, block.part,
+-                                                      block.hdr);
++                                                      &limits, block.hdr);
+               }
+       }
+       test_assert(ret < 0);
+diff --git a/src/lib-imap/test-imap-envelope.c b/src/lib-imap/test-imap-envelope.c
+index c9b92b4be2..498c6ac9f4 100644
+--- a/src/lib-imap/test-imap-envelope.c
++++ b/src/lib-imap/test-imap-envelope.c
+@@ -132,9 +132,11 @@ msg_parse(pool_t pool, const char *message)
+ 
+       input = i_stream_create_from_data(message, strlen(message));
+       parser = message_parser_init(pool, input, &parser_set);
++      struct message_part_data_limits limits = MESSAGE_PART_DATA_LIMITS_INIT;
+       while ((ret = message_parser_parse_next_block(parser, &block)) > 0) {
+               i_assert(block.part->parent == NULL);
+-              message_part_envelope_parse_from_header(pool, &envlp, block.hdr);
++              message_part_envelope_parse_from_header(pool, &envlp, &limits,
++                                                      block.hdr);
+       }
+       test_assert(ret < 0);
+ 
+diff --git a/src/lib-mail/message-part-data.c b/src/lib-mail/message-part-data.c
+index acd50e5210..c302fe568b 100644
+--- a/src/lib-mail/message-part-data.c
++++ b/src/lib-mail/message-part-data.c
+@@ -173,6 +173,7 @@ envelope_get_field(const char *name)
+ 
+ void message_part_envelope_parse_from_header(pool_t pool,
+       struct message_part_envelope **data,
++      struct message_part_data_limits *limits ATTR_UNUSED,
+       struct message_header_line *hdr)
+ {
+       struct message_part_envelope *d;
+@@ -480,6 +481,7 @@ parse_content_header(struct message_part_data *data,
+ 
+ void message_part_data_parse_from_header(pool_t pool,
+       struct message_part *part,
++      struct message_part_data_limits *limits,
+       struct message_header_line *hdr)
+ {
+       struct message_part_data *part_data;
+@@ -525,7 +527,8 @@ void message_part_data_parse_from_header(pool_t pool,
+ 
+       if (parent_rfc822) {
+               /* message/rfc822, we need the envelope */
+-              message_part_envelope_parse_from_header(pool, &part_data->envelope, hdr);
++              message_part_envelope_parse_from_header(pool, &part_data->envelope,
++                                                      limits, hdr);
+       }
+ }
+ 
+diff --git a/src/lib-mail/message-part-data.h b/src/lib-mail/message-part-data.h
+index 9513bfc2f0..1514f0daf2 100644
+--- a/src/lib-mail/message-part-data.h
++++ b/src/lib-mail/message-part-data.h
+@@ -8,6 +8,11 @@
+ 
+ struct message_header_line;
+ 
++struct message_part_data_limits {
++};
++
++#define MESSAGE_PART_DATA_LIMITS_INIT { }
++
+ struct message_part_param {
+       const char *name;
+       const char *value;
+@@ -91,11 +96,13 @@ bool message_part_is_attachment(struct message_part *part,
+ /* Update envelope data based from given header field */
+ void message_part_envelope_parse_from_header(pool_t pool,
+       struct message_part_envelope **_data,
++      struct message_part_data_limits *limits,
+       struct message_header_line *hdr);
+ 
+ /* Parse a single header. Note that this modifies part->context. */
+ void message_part_data_parse_from_header(pool_t pool,
+       struct message_part *part,
++      struct message_part_data_limits *limits,
+       struct message_header_line *hdr);
+ 
+ #endif
+diff --git a/src/lib-mail/test-message-decoder.c b/src/lib-mail/test-message-decoder.c
+index 83c267187b..ad212c8546 100644
+--- a/src/lib-mail/test-message-decoder.c
++++ b/src/lib-mail/test-message-decoder.c
+@@ -260,8 +260,10 @@ static void test_message_decoder_content_transfer_encoding(void)
+       parser = message_parser_init(pool, istream, &parser_set);
+       decoder = message_decoder_init(NULL, 0);
+ 
++      struct message_part_data_limits limits = MESSAGE_PART_DATA_LIMITS_INIT;
+       while ((ret = message_parser_parse_next_block(parser, &input)) > 0) {
+-              message_part_data_parse_from_header(pool, input.part, input.hdr);
++              message_part_data_parse_from_header(pool, input.part, &limits,
++                                                  input.hdr);
+               if (message_decoder_decode_next_block(decoder, &input, &output) &&
+                   output.hdr == NULL && output.size > 0)
+                       str_append_data(str_out, output.data, output.size);
+@@ -349,8 +351,10 @@ static void test_message_decoder_invalid_content_transfer_encoding(void)
+       parser = message_parser_init(pool, istream, &parser_set);
+       decoder = message_decoder_init(NULL, 0);
+ 
++      struct message_part_data_limits limits = MESSAGE_PART_DATA_LIMITS_INIT;
+       while ((ret = message_parser_parse_next_block(parser, &input)) > 0) {
+-              message_part_data_parse_from_header(pool, input.part, input.hdr);
++              message_part_data_parse_from_header(pool, input.part, &limits,
++                                                  input.hdr);
+               if (input.hdr != NULL &&
+                   strcasecmp(input.hdr->name, "content-transfer-encoding") == 0) {
+                       enum message_cte cte = message_decoder_parse_cte(input.hdr);
+@@ -480,8 +484,10 @@ UNICODE_REPLACEMENT_CHAR_UTF8;
+       parser = message_parser_init(pool, istream, &parser_set);
+       decoder = message_decoder_init(NULL, 0);
+ 
++      struct message_part_data_limits limits = MESSAGE_PART_DATA_LIMITS_INIT;
+       while ((ret = message_parser_parse_next_block(parser, &input)) > 0) {
+-              message_part_data_parse_from_header(pool, input.part, input.hdr);
++              message_part_data_parse_from_header(pool, input.part, &limits,
++                                                  input.hdr);
+               if (message_decoder_decode_next_block(decoder, &input, &output) &&
+                   output.hdr == NULL && output.size > 0)
+                       str_append_data(str_out, output.data, output.size);
+@@ -531,8 +537,10 @@ static void test_message_decoder_charset_mime_part_change(void)
+       parser = message_parser_init(pool, istream, &parser_set);
+       decoder = message_decoder_init(NULL, 0);
+ 
++      struct message_part_data_limits limits = MESSAGE_PART_DATA_LIMITS_INIT;
+       while ((ret = message_parser_parse_next_block(parser, &input)) > 0) {
+-              message_part_data_parse_from_header(pool, input.part, input.hdr);
++              message_part_data_parse_from_header(pool, input.part, &limits,
++                                                  input.hdr);
+               if (message_decoder_decode_next_block(decoder, &input, &output) &&
+                   output.hdr == NULL && output.size > 0)
+                       str_append_data(str_out, output.data, output.size);
+diff --git a/src/lib-mail/test-message-parser.c b/src/lib-mail/test-message-parser.c
+index 3b94034c09..b66e0a664e 100644
+--- a/src/lib-mail/test-message-parser.c
++++ b/src/lib-mail/test-message-parser.c
+@@ -51,12 +51,13 @@ static int message_parse_stream(pool_t pool, struct istream *input,
+       struct message_parser_ctx *parser;
+       struct message_block block;
+ 
++      struct message_part_data_limits limits = MESSAGE_PART_DATA_LIMITS_INIT;
+       i_zero(&block);
+       parser = message_parser_init(pool, input, set);
+       while ((ret = message_parser_parse_next_block(parser, &block)) > 0)
+               if (parse_data)
+                       message_part_data_parse_from_header(pool, block.part,
+-                                                          block.hdr);
++                                                          &limits, block.hdr);
+       message_parser_deinit(&parser, parts_r);
+       test_assert(input->stream_errno == 0);
+       return ret;
+diff --git a/src/lib-mail/test-message-part-data.c b/src/lib-mail/test-message-part-data.c
+index 470d739b5e..63228d1b3d 100644
+--- a/src/lib-mail/test-message-part-data.c
++++ b/src/lib-mail/test-message-part-data.c
+@@ -14,12 +14,13 @@ static int message_parse_stream(pool_t pool, struct istream *input,
+       struct message_parser_ctx *parser;
+       struct message_block block;
+ 
++      struct message_part_data_limits limits = MESSAGE_PART_DATA_LIMITS_INIT;
+       i_zero(&block);
+       parser = message_parser_init(pool, input, set);
+       while ((ret = message_parser_parse_next_block(parser, &block)) > 0)
+               if (parse_data)
+                       message_part_data_parse_from_header(pool, block.part,
+-                                                          block.hdr);
++                                                          &limits, block.hdr);
+       message_parser_deinit(&parser, parts_r);
+       test_assert(input->stream_errno == 0);
+       return ret;
+diff --git a/src/lib-storage/index/index-mail-headers.c b/src/lib-storage/index/index-mail-headers.c
+index a9f4d55c0d..9d608acc61 100644
+--- a/src/lib-storage/index/index-mail-headers.c
++++ b/src/lib-storage/index/index-mail-headers.c
+@@ -308,12 +308,15 @@ void index_mail_parse_header(struct message_part *part,
+       if (data->save_bodystructure_header &&
+           !data->parsed_bodystructure_header) {
+               i_assert(part != NULL);
+-              message_part_data_parse_from_header(mail->mail.data_pool, part, hdr);
++              message_part_data_parse_from_header(mail->mail.data_pool, part,
++                                                  &mail->data.part_data_limits,
++                                                  hdr);
+       }
+ 
+       if (data->save_envelope) {
+               message_part_envelope_parse_from_header(mail->mail.data_pool,
+-                                         &data->envelope_data, hdr);
++                                         &data->envelope_data,
++                                         &mail->data.part_data_limits, hdr);
+ 
+               if (hdr == NULL)
+                         index_mail_parse_finish_imap_envelope(mail);
+@@ -510,7 +513,8 @@ imap_envelope_parse_callback(struct message_header_line *hdr,
+                            struct index_mail *mail)
+ {
+       message_part_envelope_parse_from_header(mail->mail.data_pool,
+-                                 &mail->data.envelope_data, hdr);
++                                 &mail->data.envelope_data,
++                                 &mail->data.part_data_limits, hdr);
+ 
+       if (hdr == NULL)
+               index_mail_parse_finish_imap_envelope(mail);
+diff --git a/src/lib-storage/index/index-mail.c b/src/lib-storage/index/index-mail.c
+index 70060ba4f7..3911f6db1c 100644
+--- a/src/lib-storage/index/index-mail.c
++++ b/src/lib-storage/index/index-mail.c
+@@ -696,11 +696,16 @@ void index_mail_cache_pop3_data(struct mail *_mail,
+                                    &order, sizeof(order));
+ }
+ 
++struct parse_bodystructure_ctx {
++      pool_t pool;
++      struct message_part_data_limits *limits;
++};
++
+ static void parse_bodystructure_part_header(struct message_part *part,
+                                           struct message_header_line *hdr,
+-                                          pool_t pool)
++                                          struct parse_bodystructure_ctx *ctx)
+ {
+-      message_part_data_parse_from_header(pool, part, hdr);
++      message_part_data_parse_from_header(ctx->pool, part, ctx->limits, hdr);
+ }
+ 
+ static bool want_plain_bodystructure_cached(struct index_mail *mail)
+@@ -1317,9 +1322,13 @@ static int index_mail_parse_body(struct index_mail *mail,
+               /* bodystructure header is parsed, we want the body's mime
+                  headers too */
+               i_assert(data->parsed_bodystructure_header);
++              struct parse_bodystructure_ctx bsctx = {
++                      .pool = mail->mail.data_pool,
++                      .limits = &mail->data.part_data_limits,
++              };
+               message_parser_parse_body(data->parser_ctx,
+                                         parse_bodystructure_part_header,
+-                                        mail->mail.data_pool);
++                                        &bsctx);
+       } else {
+               message_parser_parse_body(data->parser_ctx,
+                       *null_message_part_header_callback, NULL);
+@@ -1860,6 +1869,8 @@ static void index_mail_init_data(struct index_mail *mail)
+       data->received_date = (time_t)-1;
+       data->sent_date.time = (uint32_t)-1;
+       data->dont_cache_field_idx = UINT_MAX;
++      data->part_data_limits = (struct message_part_data_limits)
++              MESSAGE_PART_DATA_LIMITS_INIT;
+ 
+       data->wanted_fields = mail->mail.wanted_fields;
+       if (mail->mail.wanted_headers != NULL) {
+@@ -2321,7 +2332,9 @@ void index_mail_cache_parse_continue(struct mail *_mail)
+                               mail->data.header_parsed = TRUE;
+               } else {
+                       message_part_data_parse_from_header(mail->mail.data_pool,
+-                                                      block.part, block.hdr);
++                                                      block.part,
++                                                      &mail->data.part_data_limits,
++                                                      block.hdr);
+               }
+       }
+ }
+diff --git a/src/lib-storage/index/index-mail.h b/src/lib-storage/index/index-mail.h
+index 9e2159898e..d1625b3f6e 100644
+--- a/src/lib-storage/index/index-mail.h
++++ b/src/lib-storage/index/index-mail.h
+@@ -2,6 +2,7 @@
+ #define INDEX_MAIL_H
+ 
+ #include "message-size.h"
++#include "message-part-data.h"
+ #include "mail-cache.h"
+ #include "mail-storage-private.h"
+ 
+@@ -84,6 +85,7 @@ struct index_mail_data {
+       const char *envelope, *body, *bodystructure, *guid, *filename;
+       const char *from_envelope, *body_snippet;
+       struct message_part_envelope *envelope_data;
++      struct message_part_data_limits part_data_limits;
+ 
+       uint32_t cache_flags;
+       uint64_t modseq, pvt_modseq;
+-- 
+2.47.3
+
diff --git a/patches/0012-config-doveconf-dF-Add-dovecot_storage_version.patch b/patches/0012-config-doveconf-dF-Add-dovecot_storage_version.patch
new file mode 100644 (file)
index 0000000..1cae4f2
--- /dev/null
@@ -0,0 +1,63 @@
+From ba5efe2541fa89e9cfe4e7fae4526e0f5dc18a9c Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Wed, 9 Apr 2025 14:11:42 +0300
+Subject: [PATCH 12/12] config: doveconf -dF - Add dovecot_storage_version
+
+---
+ src/config/config-parser.c | 14 ++++++++++++++
+ src/config/config-parser.h |  3 +++
+ src/config/doveconf.c      |  2 ++
+ 3 files changed, 19 insertions(+)
+
+Index: trixie/src/config/config-parser.c
+===================================================================
+--- trixie.orig/src/config/config-parser.c
++++ trixie/src/config/config-parser.c
+@@ -2958,6 +2958,20 @@ int config_parse_file(const char *path,
+               hook_config_parser_begin(&ctx);
+       } T_END;
+ 
++      if ((flags & CONFIG_PARSE_FLAG_DEFAULT_VERSION) != 0) {
++              /* Use default settings. Set dovecot_storage_version to the
++                 latest version, so it won't cause a failure.
++
++                 When building from git we don't know the latest version, so
++                 just use 9999. The version validity checks are disabled for
++                 git builds, so this should work. */
++              const char *version = version_is_valid(DOVECOT_VERSION) ?
++                      DOVECOT_VERSION : "9999";
++              if (config_apply_exact_line(&ctx, NULL, "dovecot_storage_version",
++                                          version) < 0)
++                      i_panic("Couldn't set default dovecot_storage_version: %s", ctx.error);
++      }
++
+       internal.path = "Internal config_import";
+       ctx.cur_input->input = config_import == NULL ?
+               i_stream_create_from_data("", 0) :
+Index: trixie/src/config/config-parser.h
+===================================================================
+--- trixie.orig/src/config/config-parser.h
++++ trixie/src/config/config-parser.h
+@@ -40,6 +40,9 @@ enum config_parse_flags {
+       CONFIG_PARSE_FLAG_MERGE_DEFAULT_FILTERS = BIT(9),
+       /* Ignore unknown settings in the config file. */
+       CONFIG_PARSE_FLAG_IGNORE_UNKNOWN = BIT(10),
++      /* Executing "doveconf -dF" - add default dovecot_storage_version
++         setting. */
++      CONFIG_PARSE_FLAG_DEFAULT_VERSION = BIT(11),
+ };
+ 
+ /* Used to track changed settings for a setting_parser_info. Initially only
+Index: trixie/src/config/doveconf.c
+===================================================================
+--- trixie.orig/src/config/doveconf.c
++++ trixie/src/config/doveconf.c
+@@ -1234,6 +1234,8 @@ int main(int argc, char *argv[])
+               flags |= CONFIG_PARSE_FLAG_DELAY_ERRORS;
+       if (exec_args == NULL)
+               flags |= CONFIG_PARSE_FLAG_EXTERNAL_HOOKS;
++      else if (dump_defaults)
++              flags |= CONFIG_PARSE_FLAG_DEFAULT_VERSION;
+       T_BEGIN {
+               ret = config_parse_file(dump_defaults ? NULL : config_path,
+                       flags, have_dump_filter ? &dump_filter : NULL,
diff --git a/patches/0012-lib-mail-Limit-total-address-count-per-message-to-10.patch b/patches/0012-lib-mail-Limit-total-address-count-per-message-to-10.patch
new file mode 100644 (file)
index 0000000..dce45f3
--- /dev/null
@@ -0,0 +1,71 @@
+From f42717983640b9df13ce3214cec311102d9dcb6b Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Fri, 17 Apr 2026 15:55:48 +0200
+Subject: [PATCH 12/14] lib-mail: Limit total address count per message to 100
+ 000
+
+A message with millions of addresses across all its envelope headers can
+exhaust memory when parsed (each struct message_address is ~100 bytes
+regardless of whether the raw address is only a few bytes long).
+
+Add remaining_addresses to struct message_part_data_limits, initialised to
+MESSAGE_PART_DATA_MAX_TOTAL_ADDRESSES (100 000).  Pass the remaining budget
+as max_addresses to message_address_parse_full() so parsing stops at the
+limit, then deduct the actual count parsed from the budget.
+
+Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
+---
+ src/lib-mail/message-part-data.c | 7 +++++--
+ src/lib-mail/message-part-data.h | 6 +++++-
+ 2 files changed, 10 insertions(+), 3 deletions(-)
+
+diff --git a/src/lib-mail/message-part-data.c b/src/lib-mail/message-part-data.c
+index c302fe568b..24e1a31964 100644
+--- a/src/lib-mail/message-part-data.c
++++ b/src/lib-mail/message-part-data.c
+@@ -173,7 +173,7 @@ envelope_get_field(const char *name)
+ 
+ void message_part_envelope_parse_from_header(pool_t pool,
+       struct message_part_envelope **data,
+-      struct message_part_data_limits *limits ATTR_UNUSED,
++      struct message_part_data_limits *limits,
+       struct message_header_line *hdr)
+ {
+       struct message_part_envelope *d;
+@@ -238,9 +238,12 @@ void message_part_envelope_parse_from_header(pool_t pool,
+       if (addr_p != NULL) {
+               message_address_parse_full(pool, hdr->full_value,
+                                          hdr->full_value_len,
+-                                         UINT_MAX,
++                                         limits->remaining_addresses,
+                                          MESSAGE_ADDRESS_PARSE_FLAG_FILL_MISSING,
+                                          &new_addr);
++              i_assert(new_addr.count <= limits->remaining_addresses);
++              limits->remaining_addresses -= new_addr.count;
++
+               /* Merge multiple headers the same as if they were comma
+                  separated in a single line. This is better from security
+                  point of view, because attacker could intentionally write
+diff --git a/src/lib-mail/message-part-data.h b/src/lib-mail/message-part-data.h
+index 1514f0daf2..af09ea715e 100644
+--- a/src/lib-mail/message-part-data.h
++++ b/src/lib-mail/message-part-data.h
+@@ -8,10 +8,14 @@
+ 
+ struct message_header_line;
+ 
++#define MESSAGE_PART_DATA_MAX_TOTAL_ADDRESSES 100000
++
+ struct message_part_data_limits {
++      unsigned int remaining_addresses;
+ };
+ 
+-#define MESSAGE_PART_DATA_LIMITS_INIT { }
++#define MESSAGE_PART_DATA_LIMITS_INIT \
++      { MESSAGE_PART_DATA_MAX_TOTAL_ADDRESSES }
+ 
+ struct message_part_param {
+       const char *name;
+-- 
+2.47.3
+
diff --git a/patches/0013-lib-mail-Limit-total-Content-Language-tag-count-per-.patch b/patches/0013-lib-mail-Limit-total-Content-Language-tag-count-per-.patch
new file mode 100644 (file)
index 0000000..81d39bf
--- /dev/null
@@ -0,0 +1,99 @@
+From 5292d2277474680902437ec2a23c684041cdab61 Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Fri, 17 Apr 2026 15:56:37 +0200
+Subject: [PATCH 13/14] lib-mail: Limit total Content-Language tag count per
+ message to 100 000
+
+A multipart message with many MIME parts each containing many language tags
+can exhaust memory: the per-part RFC 2231 parser had no cumulative limit.
+
+Add remaining_language_tags to struct message_part_data_limits, initialised
+to MESSAGE_PART_DATA_MAX_TOTAL_LANGUAGE_TAGS (100 000).  Break out of the
+tag-parsing loop in parse_content_language() once the budget reaches zero.
+
+Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
+---
+ src/lib-mail/message-part-data.c | 15 ++++++++++-----
+ src/lib-mail/message-part-data.h |  5 ++++-
+ 2 files changed, 14 insertions(+), 6 deletions(-)
+
+diff --git a/src/lib-mail/message-part-data.c b/src/lib-mail/message-part-data.c
+index 24e1a31964..2eed994e45 100644
+--- a/src/lib-mail/message-part-data.c
++++ b/src/lib-mail/message-part-data.c
+@@ -380,7 +380,8 @@ parse_content_disposition(struct message_part_data *data,
+ 
+ static void
+ parse_content_language(struct message_part_data *data,
+-      pool_t pool, const unsigned char *value, size_t value_len)
++      pool_t pool, const unsigned char *value, size_t value_len,
++      struct message_part_data_limits *limits)
+ {
+       struct rfc822_parser_context parser;
+       ARRAY_TYPE(const_string) langs;
+@@ -398,8 +399,11 @@ parse_content_language(struct message_part_data *data,
+ 
+       rfc822_skip_lwsp(&parser);
+       while (rfc822_parse_atom(&parser, str) >= 0) {
+-              const char *lang = p_strdup(pool, str_c(str));
++              if (limits->remaining_language_tags == 0)
++                      break;
++              limits->remaining_language_tags--;
+ 
++              const char *lang = p_strdup(pool, str_c(str));
+               array_push_back(&langs, &lang);
+               str_truncate(str, 0);
+ 
+@@ -419,7 +423,8 @@ parse_content_language(struct message_part_data *data,
+ 
+ static void
+ parse_content_header(struct message_part_data *data,
+-      pool_t pool, struct message_header_line *hdr)
++      pool_t pool, struct message_header_line *hdr,
++      struct message_part_data_limits *limits)
+ {
+       const char *name = hdr->name + strlen("Content-");
+ 
+@@ -459,7 +464,7 @@ parse_content_header(struct message_part_data *data,
+               if (strcasecmp(name, "Language") == 0 &&
+                   data->content_language == NULL) {
+                       parse_content_language(data, pool,
+-                              hdr->full_value, hdr->full_value_len);
++                              hdr->full_value, hdr->full_value_len, limits);
+               } else if (strcasecmp(name, "Location") == 0 &&
+                          data->content_location == NULL) {
+                       data->content_location =
+@@ -524,7 +529,7 @@ void message_part_data_parse_from_header(pool_t pool,
+ 
+       if (str_begins_icase_with(hdr->name, "Content-")) {
+               T_BEGIN {
+-                      parse_content_header(part_data, pool, hdr);
++                      parse_content_header(part_data, pool, hdr, limits);
+               } T_END;
+       }
+ 
+diff --git a/src/lib-mail/message-part-data.h b/src/lib-mail/message-part-data.h
+index af09ea715e..7f1f9c3e1f 100644
+--- a/src/lib-mail/message-part-data.h
++++ b/src/lib-mail/message-part-data.h
+@@ -9,13 +9,16 @@
+ struct message_header_line;
+ 
+ #define MESSAGE_PART_DATA_MAX_TOTAL_ADDRESSES 100000
++#define MESSAGE_PART_DATA_MAX_TOTAL_LANGUAGE_TAGS 100000
+ 
+ struct message_part_data_limits {
+       unsigned int remaining_addresses;
++      unsigned int remaining_language_tags;
+ };
+ 
+ #define MESSAGE_PART_DATA_LIMITS_INIT \
+-      { MESSAGE_PART_DATA_MAX_TOTAL_ADDRESSES }
++      { MESSAGE_PART_DATA_MAX_TOTAL_ADDRESSES, \
++        MESSAGE_PART_DATA_MAX_TOTAL_LANGUAGE_TAGS }
+ 
+ struct message_part_param {
+       const char *name;
+-- 
+2.47.3
+
diff --git a/patches/0014-lib-mail-Limit-total-MIME-parameter-count-per-messag.patch b/patches/0014-lib-mail-Limit-total-MIME-parameter-count-per-messag.patch
new file mode 100644 (file)
index 0000000..6279ab5
--- /dev/null
@@ -0,0 +1,130 @@
+From 6a71e37c3edae89cd446a1bb8b1f7137b5a25754 Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Fri, 17 Apr 2026 15:57:24 +0200
+Subject: [PATCH 14/14] lib-mail: Limit total MIME parameter count per message
+ to 200 000
+
+Content-Type and Content-Disposition parameters are both parsed through
+parse_mime_parameters().  A message with many MIME parts each having many
+parameters can accumulate millions of struct message_part_param entries.
+
+Add remaining_mime_params to struct message_part_data_limits (combined
+budget for both Content-Type and Content-Disposition), initialised to
+MESSAGE_PART_DATA_MAX_TOTAL_MIME_PARAMS (200 000).  Cap params_count to the
+remaining budget before allocating and deduct accordingly.
+
+Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
+---
+ src/lib-mail/message-part-data.c | 21 ++++++++++++++-------
+ src/lib-mail/message-part-data.h |  6 +++++-
+ 2 files changed, 19 insertions(+), 8 deletions(-)
+
+diff --git a/src/lib-mail/message-part-data.c b/src/lib-mail/message-part-data.c
+index 2eed994e45..e0a3676aae 100644
+--- a/src/lib-mail/message-part-data.c
++++ b/src/lib-mail/message-part-data.c
+@@ -263,7 +263,8 @@ void message_part_envelope_parse_from_header(pool_t pool,
+ static void
+ parse_mime_parameters(struct rfc822_parser_context *parser,
+       pool_t pool, const struct message_part_param **params_r,
+-      unsigned int *params_count_r)
++      unsigned int *params_count_r,
++      struct message_part_data_limits *limits)
+ {
+       const char *const *results;
+       struct message_part_param *params;
+@@ -275,6 +276,10 @@ parse_mime_parameters(struct rfc822_parser_context *parser,
+       i_assert((params_count % 2) == 0);
+       params_count /= 2;
+ 
++      if (params_count > limits->remaining_mime_params)
++              params_count = limits->remaining_mime_params;
++      limits->remaining_mime_params -= params_count;
++
+       if (params_count > 0) {
+               params = p_new(pool, struct message_part_param, params_count);
+               for (i = 0; i < params_count; i++) {
+@@ -289,7 +294,8 @@ parse_mime_parameters(struct rfc822_parser_context *parser,
+ 
+ static void
+ parse_content_type(struct message_part_data *data,
+-      pool_t pool, struct message_header_line *hdr)
++      pool_t pool, struct message_header_line *hdr,
++      struct message_part_data_limits *limits)
+ {
+       struct rfc822_parser_context parser;
+       string_t *str;
+@@ -332,7 +338,7 @@ parse_content_type(struct message_part_data *data,
+ 
+       parse_mime_parameters(&parser, pool,
+               &data->content_type_params,
+-              &data->content_type_params_count);
++              &data->content_type_params_count, limits);
+       rfc822_parser_deinit(&parser);
+ }
+ 
+@@ -357,7 +363,8 @@ parse_content_transfer_encoding(struct message_part_data *data,
+ 
+ static void
+ parse_content_disposition(struct message_part_data *data,
+-      pool_t pool, struct message_header_line *hdr)
++      pool_t pool, struct message_header_line *hdr,
++      struct message_part_data_limits *limits)
+ {
+       struct rfc822_parser_context parser;
+       string_t *str;
+@@ -374,7 +381,7 @@ parse_content_disposition(struct message_part_data *data,
+ 
+       parse_mime_parameters(&parser, pool,
+               &data->content_disposition_params,
+-              &data->content_disposition_params_count);
++              &data->content_disposition_params_count, limits);
+       rfc822_parser_deinit(&parser);
+ }
+ 
+@@ -453,7 +460,7 @@ parse_content_header(struct message_part_data *data,
+       case 't':
+       case 'T':
+               if (strcasecmp(name, "Type") == 0 && data->content_type == NULL)
+-                      parse_content_type(data, pool, hdr);
++                      parse_content_type(data, pool, hdr, limits);
+               else if (strcasecmp(name, "Transfer-Encoding") == 0 &&
+                        data->content_transfer_encoding == NULL)
+                       parse_content_transfer_encoding(data, pool, hdr);
+@@ -482,7 +489,7 @@ parse_content_header(struct message_part_data *data,
+                                                     hdr->full_value_len);
+               else if (strcasecmp(name, "Disposition") == 0 &&
+                        data->content_disposition_params == NULL)
+-                      parse_content_disposition(data, pool, hdr);
++                      parse_content_disposition(data, pool, hdr, limits);
+               break;
+       }
+ }
+diff --git a/src/lib-mail/message-part-data.h b/src/lib-mail/message-part-data.h
+index 7f1f9c3e1f..f439926c90 100644
+--- a/src/lib-mail/message-part-data.h
++++ b/src/lib-mail/message-part-data.h
+@@ -10,15 +10,19 @@ struct message_header_line;
+ 
+ #define MESSAGE_PART_DATA_MAX_TOTAL_ADDRESSES 100000
+ #define MESSAGE_PART_DATA_MAX_TOTAL_LANGUAGE_TAGS 100000
++/* Combined limit for Content-Type + Content-Disposition parameters */
++#define MESSAGE_PART_DATA_MAX_TOTAL_MIME_PARAMS 200000
+ 
+ struct message_part_data_limits {
+       unsigned int remaining_addresses;
+       unsigned int remaining_language_tags;
++      unsigned int remaining_mime_params;
+ };
+ 
+ #define MESSAGE_PART_DATA_LIMITS_INIT \
+       { MESSAGE_PART_DATA_MAX_TOTAL_ADDRESSES, \
+-        MESSAGE_PART_DATA_MAX_TOTAL_LANGUAGE_TAGS }
++        MESSAGE_PART_DATA_MAX_TOTAL_LANGUAGE_TAGS, \
++        MESSAGE_PART_DATA_MAX_TOTAL_MIME_PARAMS }
+ 
+ struct message_part_param {
+       const char *name;
+-- 
+2.47.3
+
diff --git a/patches/CVE-2025-59028.patch b/patches/CVE-2025-59028.patch
new file mode 100644 (file)
index 0000000..cfa08c6
--- /dev/null
@@ -0,0 +1,40 @@
+From dae1a1b84115b2a6146afd9a51e17137264caa0d Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Tue, 4 Nov 2025 11:34:30 +0200
+Subject: [PATCH 01/24] auth: Don't disconnect auth client when invalid base64
+ SASL input is received
+
+The base64 input comes from untrusted client. It shouldn't cause the auth
+client to disconnect, which causes other concurrent logins to be aborted.
+
+Broken by 1486c30e191ff079bfa78e7950173bb33d8073d9
+---
+ src/auth/auth-request-handler.c | 8 +++++++-
+ 1 file changed, 7 insertions(+), 1 deletion(-)
+
+Index: dovecot/src/auth/auth-request-handler.c
+===================================================================
+--- dovecot.orig/src/auth/auth-request-handler.c
++++ dovecot/src/auth/auth-request-handler.c
+@@ -715,6 +715,9 @@ int auth_request_handler_auth_begin(stru
+                       auth_request_handler_auth_fail_code(handler, request,
+                               AUTH_CLIENT_FAIL_CODE_INVALID_BASE64,
+                               "Invalid base64 data in initial response");
++                      /* The base64 input came from untrusted client. It's
++                         an expected auth failure, so don't disconnect the
++                         auth client. */
+                       return 1;
+               }
+               initial_resp_data =
+@@ -787,7 +790,10 @@ int auth_request_handler_auth_continue(s
+                       auth_request_handler_auth_fail_code(handler, request,
+                               AUTH_CLIENT_FAIL_CODE_INVALID_BASE64,
+                               "Invalid base64 data in continued response");
+-                      return -1;
++                      /* The base64 input came from untrusted client. It's
++                         an expected auth failure, so don't disconnect the
++                         auth client. */
++                      return 1;
+               }
+       }
+ 
diff --git a/patches/CVE-2025-59031.patch b/patches/CVE-2025-59031.patch
new file mode 100644 (file)
index 0000000..ea8cf1d
--- /dev/null
@@ -0,0 +1,137 @@
+From 089edc7750160bf224011c015347db1bdea435e3 Mon Sep 17 00:00:00 2001
+From: Aki Tuomi <aki.tuomi@open-xchange.com>
+Date: Thu, 8 Jan 2026 08:51:59 +0200
+Subject: [PATCH 02/24] fts: Remove decode2text.sh
+
+The script is flawed and not fit for production use, should
+recommend writing your own script, or using Apache Tika.
+---
+ src/plugins/fts/Makefile.am    |   3 -
+ src/plugins/fts/decode2text.sh | 105 ---------------------------------
+ 2 files changed, 108 deletions(-)
+ delete mode 100755 src/plugins/fts/decode2text.sh
+
+Index: dovecot/src/plugins/fts/Makefile.am
+===================================================================
+--- dovecot.orig/src/plugins/fts/Makefile.am
++++ dovecot/src/plugins/fts/Makefile.am
+@@ -65,9 +65,6 @@ xml2text_CPPFLAGS = $(AM_CPPFLAGS) $(BIN
+ xml2text_LDADD = $(LIBDOVECOT) $(BINARY_LDFLAGS)
+ xml2text_DEPENDENCIES = $(module_LTLIBRARIES) $(LIBDOVECOT_DEPS)
+ 
+-pkglibexec_SCRIPTS = decode2text.sh
+-EXTRA_DIST = $(pkglibexec_SCRIPTS)
+-
+ doveadm_module_LTLIBRARIES = \
+       lib20_doveadm_fts_plugin.la
+ 
+Index: dovecot/src/plugins/fts/decode2text.sh
+===================================================================
+--- dovecot.orig/src/plugins/fts/decode2text.sh
++++ /dev/null
+@@ -1,105 +0,0 @@
+-#!/bin/sh
+-
+-# Example attachment decoder script. The attachment comes from stdin, and
+-# the script is expected to output UTF-8 data to stdout. (If the output isn't
+-# UTF-8, everything except valid UTF-8 sequences are dropped from it.)
+-
+-# The attachment decoding is enabled by setting:
+-#
+-# plugin {
+-#   fts_decoder = decode2text
+-# }
+-# service decode2text {
+-#   executable = script /usr/local/libexec/dovecot/decode2text.sh
+-#   user = dovecot
+-#   unix_listener decode2text {
+-#     mode = 0666
+-#   }
+-# }
+-
+-libexec_dir=`dirname $0`
+-content_type=$1
+-
+-# The second parameter is the format's filename extension, which is used when
+-# found from a filename of application/octet-stream. You can also add more
+-# extensions by giving more parameters.
+-formats='application/pdf pdf
+-application/x-pdf pdf
+-application/msword doc
+-application/mspowerpoint ppt
+-application/vnd.ms-powerpoint ppt
+-application/ms-excel xls
+-application/x-msexcel xls
+-application/vnd.ms-excel xls
+-application/vnd.openxmlformats-officedocument.wordprocessingml.document docx
+-application/vnd.openxmlformats-officedocument.spreadsheetml.sheet xlsx
+-application/vnd.openxmlformats-officedocument.presentationml.presentation pptx
+-application/vnd.oasis.opendocument.text odt
+-application/vnd.oasis.opendocument.spreadsheet ods
+-application/vnd.oasis.opendocument.presentation odp
+-'
+-
+-if [ "$content_type" = "" ]; then
+-  echo "$formats"
+-  exit 0
+-fi
+-
+-fmt=`echo "$formats" | grep -w "^$content_type" | cut -d ' ' -f 2`
+-if [ "$fmt" = "" ]; then
+-  echo "Content-Type: $content_type not supported" >&2
+-  exit 1
+-fi
+-
+-# most decoders can't handle stdin directly, so write the attachment
+-# to a temp file
+-path=`mktemp`
+-trap "rm -f $path" 0 1 2 3 14 15
+-cat > $path
+-
+-xmlunzip() {
+-  name=$1
+-
+-  tempdir=`mktemp -d`
+-  if [ "$tempdir" = "" ]; then
+-    exit 1
+-  fi
+-  trap "rm -rf $path $tempdir" 0 1 2 3 14 15
+-  cd $tempdir || exit 1
+-  unzip -q "$path" 2>/dev/null || exit 0
+-  find . -name "$name" -print0 | xargs -0 cat |
+-    $libexec_dir/xml2text
+-}
+-
+-wait_timeout() {
+-  childpid=$!
+-  trap "kill -9 $childpid; rm -f $path" 1 2 3 14 15
+-  wait $childpid
+-}
+-
+-LANG=en_US.UTF-8
+-export LANG
+-if [ $fmt = "pdf" ]; then
+-  /usr/bin/pdftotext $path - 2>/dev/null&
+-  wait_timeout 2>/dev/null
+-elif [ $fmt = "doc" ]; then
+-  (/usr/bin/catdoc $path; true) 2>/dev/null&
+-  wait_timeout 2>/dev/null
+-elif [ $fmt = "ppt" ]; then
+-  (/usr/bin/catppt $path; true) 2>/dev/null&
+-  wait_timeout 2>/dev/null
+-elif [ $fmt = "xls" ]; then
+-  (/usr/bin/xls2csv $path; true) 2>/dev/null&
+-  wait_timeout 2>/dev/null
+-elif [ $fmt = "odt" -o $fmt = "ods" -o $fmt = "odp" ]; then
+-  xmlunzip "content.xml"
+-elif [ $fmt = "docx" ]; then
+-  xmlunzip "document.xml"
+-elif [ $fmt = "xlsx" ]; then
+-  xmlunzip "sharedStrings.xml"
+-elif [ $fmt = "pptx" ]; then
+-  xmlunzip "slide*.xml"
+-else
+-  echo "Buggy decoder script: $fmt not handled" >&2
+-  exit 1
+-fi
+-exit 0
diff --git a/patches/CVE-2025-59032.patch b/patches/CVE-2025-59032.patch
new file mode 100644 (file)
index 0000000..7a36b02
--- /dev/null
@@ -0,0 +1,30 @@
+From efb68fac3a9d2d04d38c4ab14dd570cf0c23923c Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Mon, 22 Dec 2025 22:25:04 +0200
+Subject: [PATCH] managesieve-login: Fix crash when command didn't finish on
+ the first call
+
+---
+ src/managesieve-login/client.c | 9 +++++----
+ 1 file changed, 5 insertions(+), 4 deletions(-)
+
+Index: dovecot/pigeonhole/src/managesieve-login/client.c
+===================================================================
+--- dovecot.orig/pigeonhole/src/managesieve-login/client.c
++++ dovecot/pigeonhole/src/managesieve-login/client.c
+@@ -336,10 +336,12 @@ static bool managesieve_client_input_nex
+                       if (args[0].type != MANAGESIEVE_ARG_EOL)
+                               ret = -1;
+               }
+-      }
+-      if (ret > 0) {
++              if (ret > 0)
++                      ret = msieve_client->cmd->func(msieve_client, args);
++      } else {
++              /* Continue unfinished command */
+               i_assert(msieve_client->cmd != NULL);
+-              ret = msieve_client->cmd->func(msieve_client, args);
++              ret = msieve_client->cmd->func(msieve_client, NULL);
+       }
+ 
+       if (ret != 0)
diff --git a/patches/CVE-2026-24031-27860-1.patch b/patches/CVE-2026-24031-27860-1.patch
new file mode 100644 (file)
index 0000000..495da7c
--- /dev/null
@@ -0,0 +1,39 @@
+From 466023fc20b05eb6a79d85b0d5efaa45a81ca48d Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Wed, 25 Feb 2026 09:33:25 +0200
+Subject: [PATCH 04/24] auth: Make struct settings_get_params params const
+
+---
+ src/auth/passdb-sql.c | 2 +-
+ src/auth/userdb-sql.c | 2 +-
+ 2 files changed, 2 insertions(+), 2 deletions(-)
+
+diff --git a/src/auth/passdb-sql.c b/src/auth/passdb-sql.c
+index db1990dc47..f3682d4926 100644
+--- a/src/auth/passdb-sql.c
++++ b/src/auth/passdb-sql.c
+@@ -182,7 +182,7 @@ static void sql_lookup_pass(struct passdb_sql_request *sql_request)
+       const struct passdb_sql_settings *set;
+       const char *error;
+ 
+-      struct settings_get_params params = {
++      const struct settings_get_params params = {
+               .escape_func = passdb_sql_escape,
+               .escape_context = module->db,
+       };
+diff --git a/src/auth/userdb-sql.c b/src/auth/userdb-sql.c
+index f97fe5e316..349f61c49e 100644
+--- a/src/auth/userdb-sql.c
++++ b/src/auth/userdb-sql.c
+@@ -128,7 +128,7 @@ static void userdb_sql_lookup(struct auth_request *auth_request,
+       const struct userdb_sql_settings *set;
+       const char *error;
+ 
+-      struct settings_get_params params = {
++      const struct settings_get_params params = {
+               .escape_func = userdb_sql_escape,
+               .escape_context = module->db,
+       };
+-- 
+2.39.5
+
diff --git a/patches/CVE-2026-24031-27860-2.patch b/patches/CVE-2026-24031-27860-2.patch
new file mode 100644 (file)
index 0000000..2f7c5a2
--- /dev/null
@@ -0,0 +1,113 @@
+From 531c0801f0431eadf91383dab25aa9a8ede0d22f Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Fri, 20 Feb 2026 18:37:38 +0200
+Subject: [PATCH 05/24] auth: passdb/userdb ldap - Fix escaping ldap filter,
+ base and bind_userdn
+
+Broken by c2ccdab8d09dec65753ee42366f48d53d7f47cfd
+---
+ src/auth/passdb-ldap.c | 23 +++++++++++++++++------
+ src/auth/userdb-ldap.c | 23 +++++++++++++++++------
+ 2 files changed, 34 insertions(+), 12 deletions(-)
+
+Index: dovecot/src/auth/passdb-ldap.c
+===================================================================
+--- dovecot.orig/src/auth/passdb-ldap.c
++++ dovecot/src/auth/passdb-ldap.c
+@@ -376,9 +376,12 @@ ldap_verify_plain(struct auth_request *r
+               return;
+       }
+ 
++      const struct settings_get_params params = {
++              .escape_func = ldap_escape,
++      };
+       const struct ldap_pre_settings *ldap_pre = NULL;
+-      if (settings_get(event, &ldap_pre_setting_parser_info, 0,
+-                       &ldap_pre, &error) < 0 ||
++      if (settings_get_params(event, &ldap_pre_setting_parser_info,
++                              &params, &ldap_pre, &error) < 0 ||
+           ldap_pre_settings_post_check(ldap_pre, DB_LDAP_LOOKUP_TYPE_PASSDB,
+                                        &error) < 0) {
+               e_error(event, "%s", error);
+@@ -414,10 +417,13 @@ static void ldap_lookup_credentials(stru
+       auth_request_ref(request);
+       ldap_request->request.ldap.auth_request = request;
+ 
++      const struct settings_get_params params = {
++              .escape_func = ldap_escape,
++      };
+       const char *error;
+       const struct ldap_pre_settings *ldap_pre = NULL;
+-      if (settings_get(event, &ldap_pre_setting_parser_info, 0,
+-                       &ldap_pre, &error) < 0 ||
++      if (settings_get_params(event, &ldap_pre_setting_parser_info, &params,
++                              &ldap_pre, &error) < 0 ||
+           ldap_pre_settings_post_check(ldap_pre, DB_LDAP_LOOKUP_TYPE_PASSDB,
+                                        &error) < 0) {
+               e_error(event, "%s", error);
+@@ -446,8 +452,13 @@ static int passdb_ldap_preinit(pool_t po
+       if (settings_get(event, &auth_passdb_post_setting_parser_info,
+                        RAW_SETTINGS, &auth_post, error_r) < 0)
+               goto failed;
+-      if (settings_get(event, &ldap_pre_setting_parser_info,
+-                       RAW_SETTINGS, &ldap_pre, error_r) < 0)
++
++      const struct settings_get_params params = {
++              .escape_func = ldap_escape,
++              .flags = RAW_SETTINGS,
++      };
++      if (settings_get_params(event, &ldap_pre_setting_parser_info,
++                              &params, &ldap_pre, error_r) < 0)
+               goto failed;
+ 
+       module = p_new(pool, struct ldap_passdb_module, 1);
+Index: dovecot/src/auth/userdb-ldap.c
+===================================================================
+--- dovecot.orig/src/auth/userdb-ldap.c
++++ dovecot/src/auth/userdb-ldap.c
+@@ -122,9 +122,12 @@ static void userdb_ldap_lookup(struct au
+       struct userdb_ldap_request *request;
+       const char *error;
+ 
++      const struct settings_get_params params = {
++              .escape_func = ldap_escape,
++      };
+       const struct ldap_pre_settings *ldap_pre = NULL;
+-      if (settings_get(event, &ldap_pre_setting_parser_info, 0,
+-                       &ldap_pre, &error) < 0 ||
++      if (settings_get_params(event, &ldap_pre_setting_parser_info, &params,
++                              &ldap_pre, &error) < 0 ||
+           ldap_pre_settings_post_check(ldap_pre, DB_LDAP_LOOKUP_TYPE_USERDB,
+                                        &error) < 0) {
+               e_error(event, "%s", error);
+@@ -258,9 +261,12 @@ userdb_ldap_iterate_init(struct auth_req
+       request = &ctx->request;
+       request->ctx = ctx;
+ 
++      const struct settings_get_params params = {
++              .escape_func = ldap_escape,
++      };
+       const struct ldap_pre_settings *ldap_pre = NULL;
+-      if (settings_get(event, &ldap_pre_setting_parser_info, 0,
+-                       &ldap_pre, &error) < 0 ||
++      if (settings_get_params(event, &ldap_pre_setting_parser_info, &params,
++                              &ldap_pre, &error) < 0 ||
+           ldap_pre_settings_post_check(ldap_pre, DB_LDAP_LOOKUP_TYPE_ITERATE,
+                                        &error) < 0) {
+               e_error(event, "%s", error);
+@@ -331,8 +337,13 @@ static int userdb_ldap_preinit(pool_t po
+       if (settings_get(event, &ldap_post_setting_parser_info,
+                        RAW_SETTINGS, &ldap_post, error_r) < 0)
+               goto failed;
+-      if (settings_get(event, &ldap_pre_setting_parser_info,
+-                       RAW_SETTINGS, &ldap_pre, error_r) < 0)
++
++      const struct settings_get_params params = {
++              .escape_func = ldap_escape,
++              .flags = RAW_SETTINGS,
++      };
++      if (settings_get_params(event, &ldap_pre_setting_parser_info,
++                              &params, &ldap_pre, error_r) < 0)
+               goto failed;
+ 
+       module = p_new(pool, struct ldap_userdb_module, 1);
diff --git a/patches/CVE-2026-24031-27860-3.patch b/patches/CVE-2026-24031-27860-3.patch
new file mode 100644 (file)
index 0000000..2776cbe
--- /dev/null
@@ -0,0 +1,32 @@
+From 926057d36ce5d3ec6833c6a6d3e3b732e0a39453 Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Mon, 23 Feb 2026 13:37:09 +0200
+Subject: [PATCH 06/24] lib-settings: settings_get_params() - Fix using
+ provided escape_func
+
+This fixes auth-sql and auth-ldap to actually do escaping.
+---
+ src/lib-settings/settings.c | 10 ++++++++--
+ 1 file changed, 8 insertions(+), 2 deletions(-)
+
+Index: dovecot/src/lib-settings/settings.c
+===================================================================
+--- dovecot.orig/src/lib-settings/settings.c
++++ dovecot/src/lib-settings/settings.c
+@@ -1540,8 +1540,14 @@ settings_var_expand_init(struct settings
+       ctx->var_params.tables_arr = array_front(&init_ctx.tables);
+       ctx->var_params.providers_arr = array_front(&init_ctx.providers);
+       ctx->var_params.contexts = array_front(&init_ctx.contexts);
+-      ctx->var_params.escape_func = init_ctx.escape_func;
+-      ctx->var_params.escape_context = init_ctx.escape_context;
++      if (ctx->escape_func != NULL) {
++              /* settings_get_params()'s escape_func overrides all others */
++              ctx->var_params.escape_func = ctx->escape_func;
++              ctx->var_params.escape_context = ctx->escape_context;
++      } else {
++              ctx->var_params.escape_func = init_ctx.escape_func;
++              ctx->var_params.escape_context = init_ctx.escape_context;
++      }
+       ctx->var_params.event = ctx->event;
+ }
+ 
diff --git a/patches/CVE-2026-24031-27860-4.patch b/patches/CVE-2026-24031-27860-4.patch
new file mode 100644 (file)
index 0000000..48970ef
--- /dev/null
@@ -0,0 +1,64 @@
+From cb09189599e28fa8f46790aeb440a35ba8414908 Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Mon, 23 Feb 2026 19:33:16 +0200
+Subject: [PATCH 07/24] auth: test-auth - Run Lua unit tests even when building
+ Lua as plugin
+
+---
+ src/auth/Makefile.am | 7 +++++--
+ src/auth/test-lua.c  | 2 +-
+ src/auth/test-main.c | 2 +-
+ 3 files changed, 7 insertions(+), 4 deletions(-)
+
+--- dovecot-2.4.1+dfsg1.orig/src/auth/Makefile.am
++++ dovecot-2.4.1+dfsg1/src/auth/Makefile.am
+@@ -131,6 +131,8 @@ auth_common_sources = \
+       $(ldap_sources) \
+       $(lua_sources)
+ 
++test_auth_ldadd_plugins =
++
+ headers = \
+       auth.h \
+       auth-cache.h \
+@@ -185,6 +187,7 @@ libauthdb_lua_la_LDFLAGS = -module -avoi
+ libauthdb_lua_la_LIBADD = $(LIBDOVECOT_LUA)
+ libauthdb_lua_la_CPPFLAGS = $(AM_CPPFLAGS) -DPLUGIN_BUILD
+ libauthdb_lua_la_SOURCES = $(lua_sources)
++test_auth_ldadd_plugins += libauthdb_lua.la
+ endif
+ 
+ libauthdb_imap_la_LDFLAGS = -module -avoid-version -shared
+@@ -226,8 +229,8 @@ test_auth_SOURCES = \
+       test-mock.c \
+       test-main.c
+ 
+-test_auth_LDADD = $(LIBDOVECOT) $(auth_libs) $(AUTH_LIBS) $(LUA_LIBS)
+-test_auth_DEPENDENCIES = $(pkglibexec_PROGRAMS) $(LIBDOVECOT_DEPS)
++test_auth_LDADD = $(test_auth_ldadd_plugins) $(LIBDOVECOT) $(LIBDOVECOT_LUA) $(auth_libs) $(AUTH_LIBS) $(LUA_LIBS)
++test_auth_DEPENDENCIES = $(test_auth_ldadd_plugins) $(pkglibexec_PROGRAMS) $(LIBDOVECOT_DEPS)
+ 
+ test_mech_SOURCES = \
+       $(auth_common_sources) \
+--- dovecot-2.4.1+dfsg1.orig/src/auth/test-lua.c
++++ dovecot-2.4.1+dfsg1/src/auth/test-lua.c
+@@ -2,7 +2,7 @@
+ 
+ #include "test-auth.h"
+ 
+-#ifdef BUILTIN_LUA
++#ifdef HAVE_LUA
+ #include "istream.h"
+ #include "auth-settings.h"
+ #include "auth-request.h"
+--- dovecot-2.4.1+dfsg1.orig/src/auth/test-main.c
++++ dovecot-2.4.1+dfsg1/src/auth/test-main.c
+@@ -22,7 +22,7 @@ int main(int argc, char *argv[])
+               TEST_NAMED(test_auth_request_var_expand)
+               TEST_NAMED(test_auth_request_fields)
+               TEST_NAMED(test_username_filter)
+-#if defined(BUILTIN_LUA)
++#if defined(HAVE_LUA)
+               TEST_NAMED(test_db_lua)
+ #endif
+               { NULL, NULL }
diff --git a/patches/CVE-2026-24031-27860-5.patch b/patches/CVE-2026-24031-27860-5.patch
new file mode 100644 (file)
index 0000000..4c7c664
--- /dev/null
@@ -0,0 +1,224 @@
+From 46fbe8a5b82d548fcf5cb352bb2510bb7292174a Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Mon, 23 Feb 2026 19:54:40 +0200
+Subject: [PATCH 08/24] auth: Rewrite ldap_escape() with a unit test
+
+---
+ src/auth/Makefile.am |  2 +
+ src/auth/db-ldap.c   | 89 +++++++++++++++++++++++++++++++++++---------
+ src/auth/db-ldap.h   |  1 +
+ src/auth/test-auth.h |  1 +
+ src/auth/test-ldap.c | 39 +++++++++++++++++++
+ src/auth/test-main.c |  3 ++
+ 6 files changed, 117 insertions(+), 18 deletions(-)
+ create mode 100644 src/auth/test-ldap.c
+
+--- dovecot-2.4.1+dfsg1.orig/src/auth/Makefile.am
++++ dovecot-2.4.1+dfsg1/src/auth/Makefile.am
+@@ -178,6 +178,7 @@ libauthdb_ldap_la_LDFLAGS = -module -avo
+ libauthdb_ldap_la_LIBADD = $(LIBDOVECOT_LDAP) $(LDAP_LIBS)
+ libauthdb_ldap_la_CPPFLAGS = $(AM_CPPFLAGS) -DPLUGIN_BUILD
+ libauthdb_ldap_la_SOURCES = $(ldap_sources)
++test_auth_ldadd_plugins += libauthdb_ldap.la
+ else
+ auth_libs += $(LIBDOVECOT_LDAP)
+ endif
+@@ -225,6 +226,7 @@ test_auth_SOURCES = \
+       test-auth-request-var-expand.c \
+       test-auth-request-fields.c \
+       test-username-filter.c \
++      test-ldap.c \
+       test-lua.c \
+       test-mock.c \
+       test-main.c
+--- dovecot-2.4.1+dfsg1.orig/src/auth/db-ldap.c
++++ dovecot-2.4.1+dfsg1/src/auth/db-ldap.c
+@@ -13,6 +13,7 @@
+ #include "str.h"
+ #include "strescape.h"
+ #include "time-util.h"
++#include "unichar.h"
+ #include "env-util.h"
+ #include "settings.h"
+ #include "ssl-settings.h"
+@@ -54,8 +55,6 @@
+ #define DB_LDAP_REQUEST_MAX_ATTEMPT_COUNT 3
+ #define DB_LDAP_ATTR_DN "~dn"
+ 
+-static const char *LDAP_ESCAPE_CHARS = "*,\\#+<>;\"()= ";
+-
+ struct db_ldap_result {
+       int refcount;
+       LDAPMessage *msg;
+@@ -1139,26 +1138,80 @@ void db_ldap_get_attribute_names(pool_t
+               *sensitive_r = array_front(&sensitive_attr_names);
+ }
+ 
+-#define IS_LDAP_ESCAPED_CHAR(c) \
+-      ((((unsigned char)(c)) & 0x80) != 0 || strchr(LDAP_ESCAPE_CHARS, (c)) != NULL)
+-
+-const char *ldap_escape(const char *str,
+-                      void *context ATTR_UNUSED)
++const char *ldap_escape(const char *input, void *context ATTR_UNUSED)
+ {
+-      string_t *ret = NULL;
+-
+-      for (const char *p = str; *p != '\0'; p++) {
+-              if (IS_LDAP_ESCAPED_CHAR(*p)) {
+-                      if (ret == NULL) {
+-                              ret = t_str_new((size_t) (p - str) + 64);
+-                              str_append_data(ret, str, (size_t) (p - str));
+-                      }
+-                      str_printfa(ret, "\\%02X", (unsigned char)*p);
+-              } else if (ret != NULL)
+-                      str_append_c(ret, *p);
++      /* This function escapes both LDAP filters and LDAP DNs. This works,
++         because both allow using the method of escaping any characters.
++         However, this isn't really recommended, since apparently some LDAP
++         servers and perhaps other tools don't handle unnecessary escaping
++         correctly.
++
++         Another issue is what to do with invalid UTF-8. LDAP filter RFC
++         suggests just escaping invalid UTF-8. LDAP DN RFC doesn't at least
++         explicitly say that. It instead seems to imply that the string is
++         just expected to be valid UTF-8. Or perhaps to use #hex-string
++         encoding, which isn't compatible with LDAP filters. Just to be safe,
++         we'll replace invalid UTF-8 input with the UTF-8 replacement
++         character.
++
++         So this function isn't perhaps the most standards compliant one, but
++         we don't really care, since this escaping is mainly intended to
++         prevent untrusted username input from breaking out of the filter or
++         DN. Valid usernames are unlikely to require escaping or to be
++         invalid UTF-8. */
++
++      /* Convert invalid UTF-8 characters to replacement characters. */
++      size_t input_len = strlen(input);
++      string_t *str = t_str_new(64);
++      if (!uni_utf8_get_valid_data((const unsigned char *)input,
++                                   input_len, str)) {
++              /* Input was not valid UTF-8 */
++              input = t_strdup(str_c(str));
++              input_len = str_len(str);
++              str_truncate(str, 0);
++      }
++
++      const char *escape_chars =
++              /* control characters */
++              "\x01\x02\x03\x04\x05\x06\x07\x08"
++              "\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10"
++              "\x11\x12\x03\x14\x15\x16\x17\x18"
++              "\x19\x1a\x1b\x1c\x1d\x1e\x1f"
++              /* filter + DN */
++              "\\"
++              /* filter only */
++              "*()"
++              /* DN only (not including leading and trailing chars) */
++              ",+<>;\"=";
++      size_t pos;
++
++      /* check the leading character separately (required by DN) */
++      if (input[0] == '#' || input[0] == ' ')
++              pos = 0;
++      else {
++              pos = strcspn(input, escape_chars);
++              if (pos == input_len) {
++                      /* the last trailing space is escaped
++                         (required by DN) */
++                      if (pos > 0 && input[pos - 1] == ' ')
++                              pos--;
++                      else
++                              return input;
++              }
+       }
+ 
+-      return ret == NULL ? str : str_c(ret);
++      do {
++              str_append_data(str, input, pos);
++              str_printfa(str, "\\%02x", (unsigned char)input[pos]);
++              input += pos + 1;
++              input_len -= pos + 1;
++              pos = strcspn(input, escape_chars);
++              /* the last trailing space is escaped (required by DN) */
++              if (pos == input_len && pos > 0 && input[pos - 1] == ' ')
++                      pos--;
++      } while (pos < input_len);
++      str_append_data(str, input, pos);
++      return str_c(str);
+ }
+ 
+ static bool
+--- dovecot-2.4.1+dfsg1.orig/src/auth/db-ldap.h
++++ dovecot-2.4.1+dfsg1/src/auth/db-ldap.h
+@@ -169,6 +169,7 @@ void db_ldap_connect_delayed(struct ldap
+ 
+ void db_ldap_enable_input(struct ldap_connection *conn, bool enable);
+ 
++const char *ldap_dn_escape(const char *str, void *context);
+ const char *ldap_escape(const char *str, void *context);
+ const char *ldap_get_error(struct ldap_connection *conn);
+ 
+--- dovecot-2.4.1+dfsg1.orig/src/auth/test-auth.h
++++ dovecot-2.4.1+dfsg1/src/auth/test-auth.h
+@@ -14,6 +14,7 @@ void test_auth_request_var_expand(void);
+ void test_auth_request_fields(void);
+ void test_db_dict_parse_cache_key(void);
+ void test_username_filter(void);
++void test_db_ldap(void);
+ void test_db_lua(void);
+ struct auth_passdb *passdb_mock(void);
+ void passdb_mock_mod_init(void);
+--- /dev/null
++++ dovecot-2.4.1+dfsg1/src/auth/test-ldap.c
+@@ -0,0 +1,39 @@
++/* Copyright (c) 2026 Dovecot authors, see the included COPYING file */
++
++#include "test-auth.h"
++
++#ifdef HAVE_LDAP
++#include "db-ldap.h"
++
++static void test_ldap_escape(void)
++{
++      struct {
++              const char *input;
++              const char *output;
++      } tests[] = {
++              { "", "" },
++              { " ", "\\20" },
++              { "  ", "\\20\\20" },
++              { "foo ", "foo\\20" },
++              { ",", "\\2c" },
++              { "s p a c e", "s p a c e" },
++              { "# start-end#", "\\23 start-end#" },
++              { "  start-end2  ", "\\20 start-end2 \\20" },
++              { "middle:,+\"\\<>;=", "middle:\\2c\\2b\\22\\5c\\3c\\3e\\3b\\3d" },
++              { "valid-utf8:\xc3\xb1", "valid-utf8:\xc3\xb1" },
++              { "Bad \xFF Characters", "Bad \xEF\xBF\xBD Characters" },
++      };
++      test_begin("ldap_escape()");
++      for (unsigned int i = 0; i < N_ELEMENTS(tests); i++) {
++              test_assert_strcmp_idx(ldap_escape(tests[i].input, NULL),
++                                     tests[i].output, i);
++      }
++      test_end();
++}
++
++void test_db_ldap(void)
++{
++      test_ldap_escape();
++}
++
++#endif
+--- dovecot-2.4.1+dfsg1.orig/src/auth/test-main.c
++++ dovecot-2.4.1+dfsg1/src/auth/test-main.c
+@@ -25,6 +25,9 @@ int main(int argc, char *argv[])
+ #if defined(HAVE_LUA)
+               TEST_NAMED(test_db_lua)
+ #endif
++#if defined(HAVE_LDAP)
++              TEST_NAMED(test_db_ldap)
++#endif
+               { NULL, NULL }
+       };
+ 
diff --git a/patches/CVE-2026-24031-27860-6.patch b/patches/CVE-2026-24031-27860-6.patch
new file mode 100644 (file)
index 0000000..782b2b6
--- /dev/null
@@ -0,0 +1,36 @@
+From 568fe0b2f57fd7563d053e5ef3127d3a969b0acc Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Tue, 24 Feb 2026 12:24:37 +0200
+Subject: [PATCH 09/24] auth: passdb sql - Fix escaping for set_credentials()
+
+This was only used by OTP SASL mechanism after successful authentication, so
+it practically couldn't be used for SQL injections.
+
+Broken by ef0c63b690e6ef9fbd53cb815dfab50d1667ba3a
+---
+ src/auth/passdb-sql.c | 9 +++++++--
+ 1 file changed, 7 insertions(+), 2 deletions(-)
+
+diff --git a/src/auth/passdb-sql.c b/src/auth/passdb-sql.c
+index f3682d4926..28291606ea 100644
+--- a/src/auth/passdb-sql.c
++++ b/src/auth/passdb-sql.c
+@@ -258,8 +258,13 @@ static void sql_set_credentials(struct auth_request *request,
+ 
+       request->mech_password = p_strdup(request->pool, new_credentials);
+ 
+-      if (settings_get(authdb_event(request), &passdb_sql_setting_parser_info, 0,
+-                       &set, &error) < 0) {
++      const struct settings_get_params params = {
++              .escape_func = passdb_sql_escape,
++              .escape_context = module->db,
++      };
++      if (settings_get_params(authdb_event(request),
++                              &passdb_sql_setting_parser_info, &params,
++                              &set, &error) < 0) {
+               e_error(authdb_event(request), "%s", error);
+               callback(FALSE, request);
+               return;
+-- 
+2.39.5
+
diff --git a/patches/CVE-2026-24031-27860-7.patch b/patches/CVE-2026-24031-27860-7.patch
new file mode 100644 (file)
index 0000000..ed76e65
--- /dev/null
@@ -0,0 +1,37 @@
+From dfce1f1e7214b97ef73fa851655d176455b836fd Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Tue, 24 Feb 2026 12:26:46 +0200
+Subject: [PATCH 10/24] auth: userdb sql - Fix escaping for user iteration
+
+This is mostly a non-issue, since userdb iteration doesn't take any
+untrusted input.
+
+Broken by ef0c63b690e6ef9fbd53cb815dfab50d1667ba3a
+---
+ src/auth/userdb-sql.c | 10 +++++++---
+ 1 file changed, 7 insertions(+), 3 deletions(-)
+
+diff --git a/src/auth/userdb-sql.c b/src/auth/userdb-sql.c
+index 349f61c49e..09bac48650 100644
+--- a/src/auth/userdb-sql.c
++++ b/src/auth/userdb-sql.c
+@@ -180,9 +180,13 @@ userdb_sql_iterate_init(struct auth_request *auth_request,
+       ctx->ctx.context = context;
+       auth_request_ref(auth_request);
+ 
+-      if (settings_get(authdb_event(auth_request),
+-                       &userdb_sql_setting_parser_info, 0,
+-                       &set, &error) < 0) {
++      const struct settings_get_params params = {
++              .escape_func = userdb_sql_escape,
++              .escape_context = module->db,
++      };
++      if (settings_get_params(authdb_event(auth_request),
++                              &userdb_sql_setting_parser_info, &params,
++                              &set, &error) < 0) {
+               e_error(authdb_event(auth_request), "%s", error);
+               ctx->ctx.failed = TRUE;
+               return &ctx->ctx;
+-- 
+2.39.5
+
diff --git a/patches/CVE-2026-24031-27860-8.patch b/patches/CVE-2026-24031-27860-8.patch
new file mode 100644 (file)
index 0000000..00a484f
--- /dev/null
@@ -0,0 +1,60 @@
+From c6b15902a0e399bd6bf035f7e8869d201e9394d2 Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Wed, 25 Feb 2026 12:40:22 +0200
+Subject: [PATCH 11/24] lib-var-expand: Add "safe" filter to prevent escaping
+ output
+
+For example ldap_base = %{passdb:next_dn | safe} to avoid escaping the DN.
+---
+ src/lib-var-expand/expansion-filter.c   | 9 +++++++++
+ src/lib-var-expand/expansion-program.c  | 3 ++-
+ src/lib-var-expand/var-expand-private.h | 1 +
+ 3 files changed, 12 insertions(+), 1 deletion(-)
+
+--- dovecot-2.4.1+dfsg1.orig/src/lib-var-expand/expansion-filter.c
++++ dovecot-2.4.1+dfsg1/src/lib-var-expand/expansion-filter.c
+@@ -1130,6 +1130,14 @@ static int fn_text(const struct var_expa
+       return 0;
+ }
+ 
++static int fn_safe(const struct var_expand_statement *stmt ATTR_UNUSED,
++                 struct var_expand_state *state,
++                 const char **error_r ATTR_UNUSED)
++{
++      state->transfer_safe = TRUE;
++      return 0;
++}
++
+ static const struct var_expand_filter var_expand_builtin_filters[] = {
+       { .name = "lookup", .filter = fn_lookup },
+       { .name = "literal", .filter = fn_literal },
+@@ -1167,6 +1175,7 @@ static const struct var_expand_filter va
+       { .name = "text", .filter = fn_text },
+       { .name = "encrypt", .filter = expansion_filter_encrypt },
+       { .name = "decrypt", .filter = expansion_filter_decrypt },
++      { .name = "safe", .filter = fn_safe },
+       { .name = NULL }
+ };
+ 
+--- dovecot-2.4.1+dfsg1.orig/src/lib-var-expand/expansion-program.c
++++ dovecot-2.4.1+dfsg1/src/lib-var-expand/expansion-program.c
+@@ -140,7 +140,8 @@ int var_expand_program_execute(string_t
+               if (state.transfer_binary)
+                       var_expand_state_set_transfer(&state, binary_to_hex(state.transfer->data, state.transfer->used));
+               if (state.transfer_set) {
+-                      if (!program->only_literal && params->escape_func != NULL) {
++                      if (!program->only_literal && !state.transfer_safe &&
++                          params->escape_func != NULL) {
+                               str_append(state.result,
+                                          params->escape_func(str_c(state.transfer),
+                                                              params->escape_context));
+--- dovecot-2.4.1+dfsg1.orig/src/lib-var-expand/var-expand-private.h
++++ dovecot-2.4.1+dfsg1/src/lib-var-expand/var-expand-private.h
+@@ -73,6 +73,7 @@ struct var_expand_state {
+       string_t *transfer;
+       bool transfer_set:1;
+       bool transfer_binary:1;
++      bool transfer_safe:1;
+ };
+ 
+ struct var_expand_statement {
diff --git a/patches/CVE-2026-27851.patch b/patches/CVE-2026-27851.patch
new file mode 100644 (file)
index 0000000..a053404
--- /dev/null
@@ -0,0 +1,46 @@
+From d75c04e8ccbeb70d66d05938665fe145175ac1b5 Mon Sep 17 00:00:00 2001
+From: Aki Tuomi <aki.tuomi@open-xchange.com>
+Date: Sun, 29 Mar 2026 19:33:45 +0300
+Subject: [PATCH] lib-var-expand: Reset safe state when transfer is unset
+
+Otherwise unsafe content is treated safe.
+---
+ src/lib-var-expand/test-var-expand.c | 5 ++++-
+ src/lib-var-expand/var-expand.c      | 1 +
+ 2 files changed, 5 insertions(+), 1 deletion(-)
+
+Index: dovecot/src/lib-var-expand/test-var-expand.c
+===================================================================
+--- dovecot.orig/src/lib-var-expand/test-var-expand.c
++++ dovecot/src/lib-var-expand/test-var-expand.c
+@@ -611,6 +611,7 @@ static void test_var_expand_escape(void)
+               { .key = "escape", .value = "'hello' \"world\"", },
+               { .key = "first", .value = "bobby" },
+               { .key = "nasty", .value = "\';-- SELECT * FROM bobby.tables" },
++              { .key = "feisty", .value = "' OR '1'='1" },
+               VAR_EXPAND_TABLE_END
+       };
+ 
+@@ -653,6 +654,10 @@ static void test_var_expand_escape(void)
+               { .in = "%{literal(\"\\\"\\\\hello\\\\world\\\"\")}", .out = "'\"\\hello\\world\"'", .ret = 0 },
+               /* Unsupported escape sequence */
+               { .in = "%{literal('\\z')}", .out = "Invalid character escape", .ret = -1 },
++
++              /* safe filter */
++              { .in = "%{feisty}", "'\\' OR \\'1\\'=\\'1'", .ret = 0 },
++              { .in = "%{clean|safe} and %{feisty}", "hello world and '\\' OR \\'1\\'=\\'1'", .ret = 0 },
+       };
+ 
+       const struct var_expand_params params = {
+Index: dovecot/src/lib-var-expand/var-expand.c
+===================================================================
+--- dovecot.orig/src/lib-var-expand/var-expand.c
++++ dovecot/src/lib-var-expand/var-expand.c
+@@ -338,6 +338,7 @@ void var_expand_state_set_transfer(struc
+ void var_expand_state_unset_transfer(struct var_expand_state *state)
+ {
+       str_truncate(state->transfer, 0);
++      state->transfer_safe = FALSE;
+       state->transfer_set = FALSE;
+ }
+ 
diff --git a/patches/CVE-2026-27855-1.patch b/patches/CVE-2026-27855-1.patch
new file mode 100644 (file)
index 0000000..f8da06f
--- /dev/null
@@ -0,0 +1,21 @@
+From f1bc3ea8ba747fcfe14ab56685d400e4e3cff130 Mon Sep 17 00:00:00 2001
+From: Aki Tuomi <aki.tuomi@open-xchange.com>
+Date: Mon, 9 Mar 2026 20:04:27 +0200
+Subject: [PATCH 21/24] auth: cache - Use translated username in
+ auth_cache_remove()
+
+---
+ src/auth/auth-cache.c | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+--- dovecot-2.4.1+dfsg1.orig/src/auth/auth-cache.c
++++ dovecot-2.4.1+dfsg1/src/auth/auth-cache.c
+@@ -474,7 +474,7 @@ void auth_cache_remove(struct auth_cache
+ {
+       struct auth_cache_node *node;
+ 
+-      key = auth_request_expand_cache_key(request, key, request->fields.user);
++      key = auth_request_expand_cache_key(request, key, request->fields.translated_username);
+       node = hash_table_lookup(cache->hash, key);
+       if (node == NULL)
+               return;
diff --git a/patches/CVE-2026-27855-2.patch b/patches/CVE-2026-27855-2.patch
new file mode 100644 (file)
index 0000000..3d948b9
--- /dev/null
@@ -0,0 +1,92 @@
+From f2119a25a439f599b0a30fc674300b8f354e5b67 Mon Sep 17 00:00:00 2001
+From: Aki Tuomi <aki.tuomi@open-xchange.com>
+Date: Wed, 11 Mar 2026 12:30:32 +0200
+Subject: [PATCH 22/24] auth: Move passdb event lifecycle handling to
+ auth_request_passdb_event_(begin|end)
+
+---
+ src/auth/auth-request.c | 39 +++++++++++++++++++++++++++------------
+ src/auth/auth-request.h |  3 +++
+ 2 files changed, 30 insertions(+), 12 deletions(-)
+
+--- dovecot-2.4.1+dfsg1.orig/src/auth/auth-request.c
++++ dovecot-2.4.1+dfsg1/src/auth/auth-request.c
+@@ -663,15 +663,10 @@ auth_request_cache_result_to_str(enum au
+       }
+ }
+ 
+-void auth_request_passdb_lookup_begin(struct auth_request *request)
++void auth_request_passdb_event_begin(struct auth_request *request)
+ {
+       struct event *event;
+ 
+-      i_assert(request->passdb != NULL);
+-      i_assert(!request->userdb_lookup);
+-
+-      request->passdb_cache_result = AUTH_REQUEST_CACHE_NONE;
+-
+       /* use passdb-specific settings during the passdb lookup */
+       request->set = request->passdb->auth_set;
+ 
+@@ -691,11 +686,35 @@ void auth_request_passdb_lookup_begin(st
+       event_set_min_log_level(event, request->passdb->auth_set->verbose ?
+                               LOG_TYPE_INFO : LOG_TYPE_WARNING);
+ 
++      array_push_back(&request->authdb_event, &event);
++}
++
++void auth_request_passdb_event_end(struct auth_request *request)
++{
++      struct event *event = authdb_event(request);
++      event_unref(&event);
++      array_pop_back(&request->authdb_event);
++
++      /* restore protocol-specific settings */
++      request->set = request->protocol_set;
++}
++
++void auth_request_passdb_lookup_begin(struct auth_request *request)
++{
++      struct event *event;
++
++      i_assert(request->passdb != NULL);
++      i_assert(!request->userdb_lookup);
++      auth_request_passdb_event_begin(request);
++
++      event = authdb_event(request);
++
++      request->passdb_cache_result = AUTH_REQUEST_CACHE_NONE;
++
+       e_debug(event_create_passthrough(event)->
+                       set_name("auth_passdb_request_started")->
+                       event(),
+               "Performing passdb lookup");
+-      array_push_back(&request->authdb_event, &event);
+ }
+ 
+ void auth_request_passdb_lookup_end(struct auth_request *request,
+@@ -713,11 +732,7 @@ void auth_request_passdb_lookup_end(stru
+                                       request->passdb_cache_result));
+       }
+       e_debug(e->event(), "Finished passdb lookup");
+-      event_unref(&event);
+-      array_pop_back(&request->authdb_event);
+-
+-      /* restore protocol-specific settings */
+-      request->set = request->protocol_set;
++      auth_request_passdb_event_end(request);
+ }
+ 
+ void auth_request_userdb_lookup_begin(struct auth_request *request)
+--- dovecot-2.4.1+dfsg1.orig/src/auth/auth-request.h
++++ dovecot-2.4.1+dfsg1/src/auth/auth-request.h
+@@ -427,6 +427,9 @@ void auth_request_log_finished(struct au
+ void auth_request_master_user_login_finish(struct auth_request *request);
+ void auth_request_fields_init(struct auth_request *request);
+ 
++void auth_request_passdb_event_begin(struct auth_request *request);
++void auth_request_passdb_event_end(struct auth_request *request);
++
+ void auth_request_passdb_lookup_begin(struct auth_request *request);
+ void auth_request_passdb_lookup_end(struct auth_request *request,
+                                   enum passdb_result result);
diff --git a/patches/CVE-2026-27855-3.patch b/patches/CVE-2026-27855-3.patch
new file mode 100644 (file)
index 0000000..cfaa7f6
--- /dev/null
@@ -0,0 +1,44 @@
+From 8fd279db61be2054c8f3e7275766717318a2df91 Mon Sep 17 00:00:00 2001
+From: Aki Tuomi <aki.tuomi@open-xchange.com>
+Date: Mon, 9 Mar 2026 21:23:29 +0200
+Subject: [PATCH 23/24] auth: Initialize set_credentials event properly
+
+Fixes update_query
+---
+ src/auth/auth-request.c       | 4 ++++
+ src/auth/auth-worker-server.c | 2 ++
+ 2 files changed, 6 insertions(+)
+
+--- dovecot-2.4.1+dfsg1.orig/src/auth/auth-request.c
++++ dovecot-2.4.1+dfsg1/src/auth/auth-request.c
+@@ -1528,6 +1528,8 @@ void auth_request_set_credentials(struct
+       struct auth_passdb *passdb = request->passdb;
+       const char *cache_key, *new_credentials;
+ 
++      auth_request_passdb_event_begin(request);
++
+       cache_key = passdb_cache == NULL ? NULL : passdb->cache_key;
+       if (cache_key != NULL)
+               auth_cache_remove(passdb_cache, request, cache_key);
+@@ -1544,6 +1546,8 @@ void auth_request_set_credentials(struct
+               /* this passdb doesn't support credentials update */
+               callback(FALSE, request);
+       }
++
++      auth_request_passdb_event_end(request);
+ }
+ 
+ static void
+--- dovecot-2.4.1+dfsg1.orig/src/auth/auth-worker-server.c
++++ dovecot-2.4.1+dfsg1/src/auth/auth-worker-server.c
+@@ -505,8 +505,10 @@ auth_worker_handle_setcred(struct auth_w
+               }
+       }
+ 
++      auth_request_passdb_event_begin(auth_request);
+       auth_request->passdb->passdb->iface.
+               set_credentials(auth_request, creds, set_credentials_callback);
++      auth_request_passdb_event_end(auth_request);
+       return TRUE;
+ }
+ 
diff --git a/patches/CVE-2026-27855-4.patch b/patches/CVE-2026-27855-4.patch
new file mode 100644 (file)
index 0000000..32c71e1
--- /dev/null
@@ -0,0 +1,30 @@
+From 535a14209810e7b0c8f94479daf580ac4c637904 Mon Sep 17 00:00:00 2001
+From: Aki Tuomi <aki.tuomi@open-xchange.com>
+Date: Wed, 11 Mar 2026 12:46:53 +0200
+Subject: [PATCH 24/24] auth: passdb-sql - Require update_query to be set when
+ used
+
+---
+ src/auth/passdb-sql.c | 6 ++++++
+ 1 file changed, 6 insertions(+)
+
+diff --git a/src/auth/passdb-sql.c b/src/auth/passdb-sql.c
+index 28291606ea..ec160e3d75 100644
+--- a/src/auth/passdb-sql.c
++++ b/src/auth/passdb-sql.c
+@@ -270,6 +270,12 @@ static void sql_set_credentials(struct auth_request *request,
+               return;
+       }
+ 
++      if (*set->update_query == '\0') {
++              e_error(authdb_event(request), "passdb_sql_update_query is empty");
++              callback(FALSE, request);
++              return;
++      }
++
+       sql_request = i_new(struct passdb_sql_request, 1);
+       sql_request->auth_request = request;
+       sql_request->callback.set_credentials = callback;
+-- 
+2.39.5
+
diff --git a/patches/CVE-2026-27856-1.patch b/patches/CVE-2026-27856-1.patch
new file mode 100644 (file)
index 0000000..ec20d45
--- /dev/null
@@ -0,0 +1,54 @@
+From 6ef282fab8af8faec75b0ff8cb87b11094642ba0 Mon Sep 17 00:00:00 2001
+From: Aki Tuomi <aki.tuomi@open-xchange.com>
+Date: Wed, 4 Mar 2026 08:05:13 +0200
+Subject: [PATCH 16/24] doveadm: client-connection - Use timing safe credential
+ check
+
+---
+ src/doveadm/client-connection-http.c | 7 +++++--
+ src/doveadm/client-connection-tcp.c  | 4 +---
+ 2 files changed, 6 insertions(+), 5 deletions(-)
+
+diff --git a/src/doveadm/client-connection-http.c b/src/doveadm/client-connection-http.c
+index 81ae9f51da..f760548653 100644
+--- a/src/doveadm/client-connection-http.c
++++ b/src/doveadm/client-connection-http.c
+@@ -973,7 +973,9 @@ doveadm_http_server_auth_basic(struct client_request_http *req,
+       value = p_strdup_printf(conn->conn.pool,
+                               "doveadm:%s", set->doveadm_password);
+       base64_encode(value, strlen(value), b64_value);
+-      if (creds->data != NULL && strcmp(creds->data, str_c(b64_value)) == 0)
++
++      if (creds->data != NULL &&
++          str_equals_timing_almost_safe(value, creds->data))
+               return TRUE;
+ 
+       e_error(conn->conn.event,
+@@ -1000,7 +1002,8 @@ doveadm_http_server_auth_api_key(struct client_request_http *req,
+       b64_value = str_new(conn->conn.pool, 32);
+       base64_encode(set->doveadm_api_key,
+                     strlen(set->doveadm_api_key), b64_value);
+-      if (creds->data != NULL && strcmp(creds->data, str_c(b64_value)) == 0)
++      if (creds->data != NULL &&
++          str_equals_timing_almost_safe(creds->data, str_c(b64_value)))
+               return TRUE;
+ 
+       e_error(conn->conn.event,
+diff --git a/src/doveadm/client-connection-tcp.c b/src/doveadm/client-connection-tcp.c
+index 936bb03f01..cb16f7214b 100644
+--- a/src/doveadm/client-connection-tcp.c
++++ b/src/doveadm/client-connection-tcp.c
+@@ -400,9 +400,7 @@ client_connection_tcp_authenticate(struct client_connection_tcp *conn)
+               return -1;
+       }
+       pass = t_strndup(data + 9, size - 9);
+-      if (strlen(pass) != strlen(set->doveadm_password) ||
+-          !mem_equals_timing_safe(pass, set->doveadm_password,
+-                                  strlen(pass))) {
++      if (!str_equals_timing_almost_safe(pass, set->doveadm_password)) {
+               e_error(conn->conn.event,
+                       "doveadm client authenticated with wrong password");
+               return -1;
+-- 
+2.39.5
+
diff --git a/patches/CVE-2026-27856-2.patch b/patches/CVE-2026-27856-2.patch
new file mode 100644 (file)
index 0000000..318e8eb
--- /dev/null
@@ -0,0 +1,54 @@
+From 6d8c50154744284304ceeab69c3951e6d6852007 Mon Sep 17 00:00:00 2001
+From: Aki Tuomi <aki.tuomi@open-xchange.com>
+Date: Wed, 4 Mar 2026 09:28:18 +0200
+Subject: [PATCH 17/24] doveadm: Use datastack for temporary b64 value
+
+There is no need to allocate it from connection pool.
+---
+ src/doveadm/client-connection-http.c | 14 +++++---------
+ 1 file changed, 5 insertions(+), 9 deletions(-)
+
+diff --git a/src/doveadm/client-connection-http.c b/src/doveadm/client-connection-http.c
+index f760548653..5d927d1979 100644
+--- a/src/doveadm/client-connection-http.c
++++ b/src/doveadm/client-connection-http.c
+@@ -960,7 +960,7 @@ doveadm_http_server_auth_basic(struct client_request_http *req,
+       struct client_connection_http *conn = req->conn;
+       const struct doveadm_settings *set = conn->conn.set;
+       string_t *b64_value;
+-      char *value;
++      const char *value;
+ 
+       if (*set->doveadm_password == '\0') {
+               e_error(conn->conn.event,
+@@ -969,13 +969,11 @@ doveadm_http_server_auth_basic(struct client_request_http *req,
+               return FALSE;
+       }
+ 
+-      b64_value = str_new(conn->conn.pool, 32);
+-      value = p_strdup_printf(conn->conn.pool,
+-                              "doveadm:%s", set->doveadm_password);
+-      base64_encode(value, strlen(value), b64_value);
++      value = t_strdup_printf("doveadm:%s", set->doveadm_password);
++      b64_value = t_base64_encode_str(0, UINT_MAX, value);
+ 
+       if (creds->data != NULL &&
+-          str_equals_timing_almost_safe(value, creds->data))
++          str_equals_timing_almost_safe(str_c(b64_value), creds->data))
+               return TRUE;
+ 
+       e_error(conn->conn.event,
+@@ -999,9 +997,7 @@ doveadm_http_server_auth_api_key(struct client_request_http *req,
+               return FALSE;
+       }
+ 
+-      b64_value = str_new(conn->conn.pool, 32);
+-      base64_encode(set->doveadm_api_key,
+-                    strlen(set->doveadm_api_key), b64_value);
++      b64_value = t_base64_encode_str(0, UINT_MAX, set->doveadm_api_key);
+       if (creds->data != NULL &&
+           str_equals_timing_almost_safe(creds->data, str_c(b64_value)))
+               return TRUE;
+-- 
+2.39.5
+
diff --git a/patches/CVE-2026-27856-3.patch b/patches/CVE-2026-27856-3.patch
new file mode 100644 (file)
index 0000000..e0b5623
--- /dev/null
@@ -0,0 +1,26 @@
+From cc628976210de17bd26b4d9761ea879ce78b7980 Mon Sep 17 00:00:00 2001
+From: Aki Tuomi <aki.tuomi@open-xchange.com>
+Date: Wed, 4 Mar 2026 14:39:43 +0200
+Subject: [PATCH 18/24] doveadm: client-connection - Get API key from
+ per-connection settings
+
+---
+ src/doveadm/client-connection-http.c | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+diff --git a/src/doveadm/client-connection-http.c b/src/doveadm/client-connection-http.c
+index 5d927d1979..dfbc3528d7 100644
+--- a/src/doveadm/client-connection-http.c
++++ b/src/doveadm/client-connection-http.c
+@@ -987,7 +987,7 @@ doveadm_http_server_auth_api_key(struct client_request_http *req,
+                                const struct http_auth_credentials *creds)
+ {
+       struct client_connection_http *conn = req->conn;
+-      const struct doveadm_settings *set = doveadm_settings;
++      const struct doveadm_settings *set = conn->conn.set;
+       string_t *b64_value;
+ 
+       if (*set->doveadm_api_key == '\0') {
+-- 
+2.39.5
+
diff --git a/patches/CVE-2026-27857-1.patch b/patches/CVE-2026-27857-1.patch
new file mode 100644 (file)
index 0000000..fb2ac2f
--- /dev/null
@@ -0,0 +1,27 @@
+From bc91df5f91e8b83c58f138799d39be687271106d Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Fri, 6 Mar 2026 17:06:45 +0200
+Subject: [PATCH 1/2] plugins: imap-filter-sieve: imap-filter-sieve - Adjust to
+ imap_parser_create() API change
+
+---
+ src/plugins/imap-filter-sieve/cmd-filter-sieve.c | 3 ++-
+ 1 file changed, 2 insertions(+), 1 deletion(-)
+
+diff --git a/pigeonhole/src/plugins/imap-filter-sieve/cmd-filter-sieve.c b/pigeonhole/src/plugins/imap-filter-sieve/cmd-filter-sieve.c
+index 6965b511..33d6804c 100644
+--- a/pigeonhole/src/plugins/imap-filter-sieve/cmd-filter-sieve.c
++++ b/pigeonhole/src/plugins/imap-filter-sieve/cmd-filter-sieve.c
+@@ -379,7 +379,8 @@ bool cmd_filter_sieve(struct client_command_context *cmd)
+          asynchronously the same way as APPEND does. */
+       client->input_lock = cmd;
+       ctx->parser = imap_parser_create(client->input, client->output,
+-                                       client->set->imap_max_line_length);
++                                       client->set->imap_max_line_length,
++                                       NULL);
+       if (client->set->imap_literal_minus)
+               imap_parser_enable_literal_minus(ctx->parser);
+       o_stream_unset_flush_callback(client->output);
+-- 
+2.39.5
+
diff --git a/patches/CVE-2026-27857-2.patch b/patches/CVE-2026-27857-2.patch
new file mode 100644 (file)
index 0000000..37e7b7d
--- /dev/null
@@ -0,0 +1,253 @@
+From ad8e6542518acee4e1a803001b232c03732cdaff Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Fri, 6 Mar 2026 15:25:14 +0200
+Subject: [PATCH 12/24] lib-imap, global: Add params parameter to
+ imap_parser_create()
+
+---
+ src/imap-login/imap-login-client.c     |  4 ++--
+ src/imap-login/imap-login-cmd-id.c     |  4 +++-
+ src/imap/cmd-append.c                  |  3 ++-
+ src/imap/cmd-setmetadata.c             |  3 ++-
+ src/imap/imap-client.c                 |  3 ++-
+ src/lib-imap-client/imapc-connection.c |  3 ++-
+ src/lib-imap-storage/imap-msgpart.c    |  2 +-
+ src/lib-imap/imap-bodystructure.c      |  4 ++--
+ src/lib-imap/imap-envelope.c           |  2 +-
+ src/lib-imap/imap-parser.c             |  3 ++-
+ src/lib-imap/imap-parser.h             |  6 +++++-
+ src/lib-imap/test-imap-parser.c        | 10 +++++-----
+ src/plugins/virtual/virtual-config.c   |  2 +-
+ 13 files changed, 30 insertions(+), 19 deletions(-)
+
+Index: dovecot/src/imap-login/imap-login-client.c
+===================================================================
+--- dovecot.orig/src/imap-login/imap-login-client.c
++++ dovecot/src/imap-login/imap-login-client.c
+@@ -382,7 +382,7 @@ static int imap_client_create(struct cli
+       imap_client->parser =
+               imap_parser_create(imap_client->common.input,
+                                  imap_client->common.output,
+-                                 IMAP_LOGIN_MAX_LINE_LENGTH);
++                                 IMAP_LOGIN_MAX_LINE_LENGTH, NULL);
+       struct settings_instance *set_instance = settings_instance_find(client->event);
+       if (set_instance == NULL) {
+               set_instance = settings_instance_new(
+@@ -468,7 +468,7 @@ static void imap_client_starttls(struct
+       imap_client->parser =
+               imap_parser_create(imap_client->common.input,
+                                  imap_client->common.output,
+-                                 IMAP_LOGIN_MAX_LINE_LENGTH);
++                                 IMAP_LOGIN_MAX_LINE_LENGTH, NULL);
+ 
+       /* CRLF is lost from buffer when streams are reopened. */
+       imap_client->skip_line = FALSE;
+Index: dovecot/src/imap-login/imap-login-cmd-id.c
+===================================================================
+--- dovecot.orig/src/imap-login/imap-login-cmd-id.c
++++ dovecot/src/imap-login/imap-login-cmd-id.c
+@@ -346,7 +346,8 @@ int cmd_id(struct imap_client *client)
+               id->params->pool = param_pool;
+               id->parser = imap_parser_create(client->common.input,
+                                               client->common.output,
+-                                              IMAP_LOGIN_MAX_LINE_LENGTH);
++                                              IMAP_LOGIN_MAX_LINE_LENGTH,
++                                              NULL);
+               if (client->set->imap_literal_minus)
+                       imap_parser_enable_literal_minus(id->parser);
+               parser_flags = IMAP_PARSE_FLAG_STOP_AT_LIST;
+Index: dovecot/src/imap/cmd-append.c
+===================================================================
+--- dovecot.orig/src/imap/cmd-append.c
++++ dovecot/src/imap/cmd-append.c
+@@ -1063,7 +1063,8 @@ static bool cmd_append_full(struct clien
+       o_stream_unset_flush_callback(client->output);
+ 
+       ctx->save_parser = imap_parser_create(client->input, client->output,
+-                                            client->set->imap_max_line_length);
++                                            client->set->imap_max_line_length,
++                                            NULL);
+       if (client->set->imap_literal_minus)
+               imap_parser_enable_literal_minus(ctx->save_parser);
+ 
+Index: dovecot/src/imap/cmd-setmetadata.c
+===================================================================
+--- dovecot.orig/src/imap/cmd-setmetadata.c
++++ dovecot/src/imap/cmd-setmetadata.c
+@@ -289,7 +289,8 @@ cmd_setmetadata_start(struct imap_setmet
+          asynchronously the same way as APPEND does. */
+       client->input_lock = cmd;
+       ctx->parser = imap_parser_create(client->input, client->output,
+-                                       client->set->imap_max_line_length);
++                                       client->set->imap_max_line_length,
++                                       NULL);
+       if (client->set->imap_literal_minus)
+               imap_parser_enable_literal_minus(ctx->parser);
+       o_stream_unset_flush_callback(client->output);
+Index: dovecot/src/imap/imap-client.c
+===================================================================
+--- dovecot.orig/src/imap/imap-client.c
++++ dovecot/src/imap/imap-client.c
+@@ -975,7 +975,8 @@ client_command_new(struct client *client
+       } else {
+               cmd->parser =
+                       imap_parser_create(client->input, client->output,
+-                                         client->set->imap_max_line_length);
++                                         client->set->imap_max_line_length,
++                                         NULL);
+               if (client->set->imap_literal_minus)
+                       imap_parser_enable_literal_minus(cmd->parser);
+       }
+Index: dovecot/src/lib-imap-client/imapc-connection.c
+===================================================================
+--- dovecot.orig/src/lib-imap-client/imapc-connection.c
++++ dovecot/src/lib-imap-client/imapc-connection.c
+@@ -1876,7 +1876,8 @@ static void imapc_connection_connect_nex
+       o_stream_set_flush_callback(conn->output, imapc_connection_connected,
+                                   conn);
+       conn->parser = imap_parser_create(conn->input, NULL,
+-                                        conn->client->set->imapc_max_line_length);
++                                        conn->client->set->imapc_max_line_length,
++                                        NULL);
+       conn->to = timeout_add(conn->client->set->imapc_connection_timeout_interval_msecs,
+                              imapc_connection_timeout, conn);
+       conn->to_output = timeout_add(conn->client->set->imapc_max_idle_time_secs*1000,
+Index: dovecot/src/lib-imap-storage/imap-msgpart.c
+===================================================================
+--- dovecot.orig/src/lib-imap-storage/imap-msgpart.c
++++ dovecot/src/lib-imap-storage/imap-msgpart.c
+@@ -149,7 +149,7 @@ imap_msgpart_get_header_fields(pool_t po
+       int result = 0;
+ 
+       input = i_stream_create_from_data(header_list, strlen(header_list));
+-      parser = imap_parser_create(input, NULL, SIZE_MAX);
++      parser = imap_parser_create(input, NULL, SIZE_MAX, NULL);
+ 
+       if (imap_parser_finish_line(parser, 0, 0, &args) > 0 &&
+           imap_arg_get_list_full(args, &hdr_list, &list_count) &&
+Index: dovecot/src/lib-imap/imap-bodystructure.c
+===================================================================
+--- dovecot.orig/src/lib-imap/imap-bodystructure.c
++++ dovecot/src/lib-imap/imap-bodystructure.c
+@@ -722,7 +722,7 @@ int imap_bodystructure_parse_full(const
+       input = i_stream_create_from_data(bodystructure, strlen(bodystructure));
+       (void)i_stream_read(input);
+ 
+-      parser = imap_parser_create(input, NULL, SIZE_MAX);
++      parser = imap_parser_create(input, NULL, SIZE_MAX, NULL);
+       ret = imap_parser_finish_line(parser, 0,
+                                     IMAP_PARSE_FLAG_LITERAL_TYPE, &args);
+       if (ret < 0) {
+@@ -972,7 +972,7 @@ int imap_body_parse_from_bodystructure(c
+       input = i_stream_create_from_data(bodystructure, strlen(bodystructure));
+       (void)i_stream_read(input);
+ 
+-      parser = imap_parser_create(input, NULL, SIZE_MAX);
++      parser = imap_parser_create(input, NULL, SIZE_MAX, NULL);
+       ret = imap_parser_finish_line(parser, 0, IMAP_PARSE_FLAG_NO_UNESCAPE |
+                                     IMAP_PARSE_FLAG_LITERAL_TYPE, &args);
+       if (ret < 0) {
+Index: dovecot/src/lib-imap/imap-envelope.c
+===================================================================
+--- dovecot.orig/src/lib-imap/imap-envelope.c
++++ dovecot/src/lib-imap/imap-envelope.c
+@@ -222,7 +222,7 @@ bool imap_envelope_parse(const char *env
+       input = i_stream_create_from_data(envelope, strlen(envelope));
+       (void)i_stream_read(input);
+ 
+-      parser = imap_parser_create(input, NULL, SIZE_MAX);
++      parser = imap_parser_create(input, NULL, SIZE_MAX, NULL);
+       ret = imap_parser_finish_line(parser, 0,
+                                     IMAP_PARSE_FLAG_LITERAL_TYPE, &args);
+       if (ret < 0) {
+Index: dovecot/src/lib-imap/imap-parser.c
+===================================================================
+--- dovecot.orig/src/lib-imap/imap-parser.c
++++ dovecot/src/lib-imap/imap-parser.c
+@@ -69,7 +69,8 @@ struct imap_parser {
+ 
+ struct imap_parser *
+ imap_parser_create(struct istream *input, struct ostream *output,
+-                 size_t max_line_size)
++                 size_t max_line_size,
++                 const struct imap_parser_params *params ATTR_UNUSED)
+ {
+       struct imap_parser *parser;
+ 
+Index: dovecot/src/lib-imap/imap-parser.h
+===================================================================
+--- dovecot.orig/src/lib-imap/imap-parser.h
++++ dovecot/src/lib-imap/imap-parser.h
+@@ -38,6 +38,9 @@ enum imap_parser_error {
+       IMAP_PARSE_ERROR_LITERAL_TOO_BIG
+ };
+ 
++struct imap_parser_params {
++};
++
+ struct imap_parser;
+ 
+ /* Create new IMAP argument parser. output is used for sending command
+@@ -53,7 +56,8 @@ struct imap_parser;
+    2 * max_line_size. */
+ struct imap_parser *
+ imap_parser_create(struct istream *input, struct ostream *output,
+-                 size_t max_line_size) ATTR_NULL(2);
++                 size_t max_line_size,
++                 const struct imap_parser_params *params);
+ void imap_parser_ref(struct imap_parser *parser);
+ void imap_parser_unref(struct imap_parser **parser);
+ 
+Index: dovecot/src/lib-imap/test-imap-parser.c
+===================================================================
+--- dovecot.orig/src/lib-imap/test-imap-parser.c
++++ dovecot/src/lib-imap/test-imap-parser.c
+@@ -16,7 +16,7 @@ static void test_imap_parser_crlf(void)
+ 
+       test_begin("imap parser crlf handling");
+       input = test_istream_create(test_input);
+-      parser = imap_parser_create(input, NULL, 1024);
++      parser = imap_parser_create(input, NULL, 1024, NULL);
+ 
+       /* must return -2 until LF is read */
+       for (i = 0; test_input[i] != '\n'; i++) {
+@@ -60,7 +60,7 @@ static void test_imap_parser_partial_lis
+ 
+       test_begin("imap parser partial list");
+       input = test_istream_create(test_input);
+-      parser = imap_parser_create(input, NULL, 1024);
++      parser = imap_parser_create(input, NULL, 1024, NULL);
+ 
+       (void)i_stream_read(input);
+       test_assert(imap_parser_read_args(parser, 0,
+@@ -128,7 +128,7 @@ static void test_imap_parser_read_tag_cm
+               if (tests[i].type != COMMAND) {
+                       input = test_istream_create(tests[i].input);
+                       test_assert(i_stream_read(input) > 0);
+-                      parser = imap_parser_create(input, NULL, 1024);
++                      parser = imap_parser_create(input, NULL, 1024, NULL);
+                       ret = imap_parser_read_tag(parser, &atom);
+                       test_assert_idx(ret == tests[i].ret, i);
+                       test_assert_idx(ret <= 0 || strcmp(tests[i].tag, atom) == 0, i);
+@@ -139,7 +139,7 @@ static void test_imap_parser_read_tag_cm
+               if (tests[i].type != TAG) {
+                       input = test_istream_create(tests[i].input);
+                       test_assert(i_stream_read(input) > 0);
+-                      parser = imap_parser_create(input, NULL, 1024);
++                      parser = imap_parser_create(input, NULL, 1024, NULL);
+                       ret = imap_parser_read_command_name(parser, &atom);
+                       test_assert_idx(ret == tests[i].ret, i);
+                       test_assert_idx(ret <= 0 || strcmp(tests[i].tag, atom) == 0, i);
+Index: dovecot/src/plugins/virtual/virtual-config.c
+===================================================================
+--- dovecot.orig/src/plugins/virtual/virtual-config.c
++++ dovecot/src/plugins/virtual/virtual-config.c
+@@ -54,7 +54,7 @@ virtual_search_args_parse(const string_t
+       input = i_stream_create_from_data(str_data(rule), str_len(rule));
+       (void)i_stream_read(input);
+ 
+-      imap_parser = imap_parser_create(input, NULL, SIZE_MAX);
++      imap_parser = imap_parser_create(input, NULL, SIZE_MAX, NULL);
+       ret = imap_parser_finish_line(imap_parser, 0,  0, &args);
+       if (ret < 0) {
+               sargs = NULL;
diff --git a/patches/CVE-2026-27857-3.patch b/patches/CVE-2026-27857-3.patch
new file mode 100644 (file)
index 0000000..2a96640
--- /dev/null
@@ -0,0 +1,154 @@
+From bb845a61f81be978f641b79058d4bae06f27ee39 Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Fri, 6 Mar 2026 15:32:29 +0200
+Subject: [PATCH 13/24] lib-imap: Add imap_parser_params.list_count_limit
+
+---
+ src/lib-imap/imap-parser.c      | 15 ++++++++++-
+ src/lib-imap/imap-parser.h      |  6 +++++
+ src/lib-imap/test-imap-parser.c | 46 +++++++++++++++++++++++++++++++++
+ 3 files changed, 66 insertions(+), 1 deletion(-)
+
+--- dovecot-2.4.1+dfsg1.orig/src/lib-imap/imap-parser.c
++++ dovecot-2.4.1+dfsg1/src/lib-imap/imap-parser.c
+@@ -39,6 +39,7 @@ struct imap_parser {
+       struct istream *input;
+       struct ostream *output;
+       size_t max_line_size;
++      unsigned int list_count_limit;
+         enum imap_parser_flags flags;
+ 
+       /* reset by imap_parser_reset(): */
+@@ -46,6 +47,7 @@ struct imap_parser {
+       ARRAY_TYPE(imap_arg_list) root_list;
+         ARRAY_TYPE(imap_arg_list) *cur_list;
+       struct imap_arg *list_arg;
++      unsigned int list_count;
+ 
+       enum arg_parse_type cur_type;
+       size_t cur_pos; /* parser position in input buffer */
+@@ -70,7 +72,7 @@ struct imap_parser {
+ struct imap_parser *
+ imap_parser_create(struct istream *input, struct ostream *output,
+                  size_t max_line_size,
+-                 const struct imap_parser_params *params ATTR_UNUSED)
++                 const struct imap_parser_params *params)
+ {
+       struct imap_parser *parser;
+ 
+@@ -81,6 +83,10 @@ imap_parser_create(struct istream *input
+       parser->input = input;
+       parser->output = output;
+       parser->max_line_size = max_line_size;
++      if (params != NULL && params->list_count_limit > 0)
++              parser->list_count_limit = params->list_count_limit;
++      else
++              parser->list_count_limit = UINT_MAX;
+ 
+       p_array_init(&parser->root_list, parser->pool, LIST_INIT_COUNT);
+       parser->cur_list = &parser->root_list;
+@@ -122,6 +128,7 @@ void imap_parser_reset(struct imap_parse
+       p_array_init(&parser->root_list, parser->pool, LIST_INIT_COUNT);
+       parser->cur_list = &parser->root_list;
+       parser->list_arg = NULL;
++      parser->list_count = 0;
+ 
+       parser->cur_type = ARG_PARSE_NONE;
+       parser->cur_pos = 0;
+@@ -210,6 +217,12 @@ static bool imap_parser_close_list(struc
+               parser->error = IMAP_PARSE_ERROR_BAD_SYNTAX;
+               return FALSE;
+       }
++      if (parser->list_count >= parser->list_count_limit) {
++              parser->error_msg = "Too many '('";
++              parser->error = IMAP_PARSE_ERROR_BAD_SYNTAX;
++              return FALSE;
++      }
++      parser->list_count++;
+ 
+       arg = imap_arg_create(parser);
+       arg->type = IMAP_ARG_EOL;
+--- dovecot-2.4.1+dfsg1.orig/src/lib-imap/imap-parser.h
++++ dovecot-2.4.1+dfsg1/src/lib-imap/imap-parser.h
+@@ -39,6 +39,12 @@ enum imap_parser_error {
+ };
+ 
+ struct imap_parser_params {
++      /* How many open lists ('(' chars) to allow before faililng the parsing.
++         0 means unlimited. This is mainly used to prevent excessive memory
++         usage in imap-login process. In imap process there are many other
++         ways to increase memory usage, so we let the max_line_size be the
++         only limit. */
++      unsigned int list_count_limit;
+ };
+ 
+ struct imap_parser;
+--- dovecot-2.4.1+dfsg1.orig/src/lib-imap/test-imap-parser.c
++++ dovecot-2.4.1+dfsg1/src/lib-imap/test-imap-parser.c
+@@ -2,6 +2,7 @@
+ 
+ #include "lib.h"
+ #include "istream.h"
++#include "istream-chain.h"
+ #include "imap-parser.h"
+ #include "test-common.h"
+ 
+@@ -79,6 +80,50 @@ static void test_imap_parser_partial_lis
+       test_end();
+ }
+ 
++static void test_imap_parser_list_limit(void)
++{
++      struct {
++              const char *input;
++              int ret;
++      } tests[] = {
++              { "(())\r\n", 1 },
++              { "((()))\r\n", -1 },
++      };
++      struct istream_chain *chain;
++      struct istream *chain_input;
++      struct imap_parser *parser;
++      const struct imap_arg *args;
++
++      test_begin("imap parser list limit");
++      struct imap_parser_params params = {
++              .list_count_limit = 2,
++      };
++
++      for (unsigned int i = 0; i < N_ELEMENTS(tests); i++) {
++              chain_input = i_stream_create_chain(&chain, SIZE_MAX);
++              parser = imap_parser_create(chain_input, NULL, 1024, &params);
++
++              for (unsigned int j = 0; j < 2; j++) {
++                      struct istream *input =
++                              test_istream_create(tests[i].input);
++                      i_stream_chain_append(chain, input);
++                      i_stream_unref(&input);
++
++                      (void)i_stream_read(chain_input);
++
++                      test_assert_cmp(imap_parser_read_args(parser, 0, 0, &args), ==, tests[i].ret);
++                      /* skip over CRLF */
++                      i_stream_skip(chain_input, i_stream_get_data_size(chain_input));
++
++                      /* make sure parser reset works */
++                      imap_parser_reset(parser);
++              }
++              imap_parser_unref(&parser);
++              i_stream_destroy(&chain_input);
++      }
++      test_end();
++}
++
+ static void test_imap_parser_read_tag_cmd(void)
+ {
+       enum read_type {
+@@ -155,6 +200,7 @@ int main(void)
+       static void (*const test_functions[])(void) = {
+               test_imap_parser_crlf,
+               test_imap_parser_partial_list,
++              test_imap_parser_list_limit,
+               test_imap_parser_read_tag_cmd,
+               NULL
+       };
diff --git a/patches/CVE-2026-27857-4.patch b/patches/CVE-2026-27857-4.patch
new file mode 100644 (file)
index 0000000..1734f02
--- /dev/null
@@ -0,0 +1,83 @@
+From ab084588f615b7f2bb6e18c8ee9176511b647aca Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Fri, 6 Mar 2026 15:35:12 +0200
+Subject: [PATCH 14/24] imap-login: Limit the number of open IMAP parser lists
+
+This prevents attackers from using a large number of '(' in a command to
+grow memory usage excessively.
+---
+ src/imap-login/imap-login-client.c | 10 ++++++++--
+ src/imap-login/imap-login-client.h |  4 ++++
+ src/imap-login/imap-login-cmd-id.c |  6 +++++-
+ 3 files changed, 17 insertions(+), 3 deletions(-)
+
+Index: dovecot/src/imap-login/imap-login-client.c
+===================================================================
+--- dovecot.orig/src/imap-login/imap-login-client.c
++++ dovecot/src/imap-login/imap-login-client.c
+@@ -379,10 +379,13 @@ static int imap_client_create(struct cli
+               return -1;
+       }
+ 
++      struct imap_parser_params params = {
++              .list_count_limit = IMAP_LOGIN_LIST_COUNT_LIMIT,
++      };
+       imap_client->parser =
+               imap_parser_create(imap_client->common.input,
+                                  imap_client->common.output,
+-                                 IMAP_LOGIN_MAX_LINE_LENGTH, NULL);
++                                 IMAP_LOGIN_MAX_LINE_LENGTH, &params);
+       struct settings_instance *set_instance = settings_instance_find(client->event);
+       if (set_instance == NULL) {
+               set_instance = settings_instance_new(
+@@ -464,11 +467,14 @@ static void imap_client_starttls(struct
+       struct imap_client *imap_client =
+               container_of(client, struct imap_client, common);
+ 
++      struct imap_parser_params params = {
++              .list_count_limit = IMAP_LOGIN_LIST_COUNT_LIMIT,
++      };
+       imap_parser_unref(&imap_client->parser);
+       imap_client->parser =
+               imap_parser_create(imap_client->common.input,
+                                  imap_client->common.output,
+-                                 IMAP_LOGIN_MAX_LINE_LENGTH, NULL);
++                                 IMAP_LOGIN_MAX_LINE_LENGTH, &params);
+ 
+       /* CRLF is lost from buffer when streams are reopened. */
+       imap_client->skip_line = FALSE;
+Index: dovecot/src/imap-login/imap-login-client.h
+===================================================================
+--- dovecot.orig/src/imap-login/imap-login-client.h
++++ dovecot/src/imap-login/imap-login-client.h
+@@ -11,6 +11,10 @@
+ /* maximum length for IMAP command line. */
+ #define IMAP_LOGIN_MAX_LINE_LENGTH 8192
+ 
++/* Maximum number of '(' allowed in an IMAP command. Pre-login only uses
++   lists in the ID command. */
++#define IMAP_LOGIN_LIST_COUNT_LIMIT 1
++
+ enum imap_client_id_state {
+       IMAP_CLIENT_ID_STATE_LIST = 0,
+       IMAP_CLIENT_ID_STATE_KEY,
+Index: dovecot/src/imap-login/imap-login-cmd-id.c
+===================================================================
+--- dovecot.orig/src/imap-login/imap-login-cmd-id.c
++++ dovecot/src/imap-login/imap-login-cmd-id.c
+@@ -344,10 +344,14 @@ int cmd_id(struct imap_client *client)
+               client->cmd_id = id = i_new(struct imap_client_cmd_id, 1);
+               id->params = p_new(param_pool, struct imap_id_params, 1);
+               id->params->pool = param_pool;
++
++              struct imap_parser_params params = {
++                      .list_count_limit = IMAP_LOGIN_LIST_COUNT_LIMIT,
++              };
+               id->parser = imap_parser_create(client->common.input,
+                                               client->common.output,
+                                               IMAP_LOGIN_MAX_LINE_LENGTH,
+-                                              NULL);
++                                              &params);
+               if (client->set->imap_literal_minus)
+                       imap_parser_enable_literal_minus(id->parser);
+               parser_flags = IMAP_PARSE_FLAG_STOP_AT_LIST;
diff --git a/patches/CVE-2026-27857-5.patch b/patches/CVE-2026-27857-5.patch
new file mode 100644 (file)
index 0000000..4496bf6
--- /dev/null
@@ -0,0 +1,53 @@
+From e0b953af76d754c2984c2867b71fccfef5231bf5 Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Mon, 2 Mar 2026 13:50:24 +0200
+Subject: [PATCH 15/24] global: Use const for struct imap_parser_params params
+
+---
+ src/imap-login/imap-login-client.c | 4 ++--
+ src/imap-login/imap-login-cmd-id.c | 2 +-
+ src/lib-imap/test-imap-parser.c    | 2 +-
+ 3 files changed, 4 insertions(+), 4 deletions(-)
+
+--- dovecot-2.4.1+dfsg1.orig/src/imap-login/imap-login-client.c
++++ dovecot-2.4.1+dfsg1/src/imap-login/imap-login-client.c
+@@ -379,7 +379,7 @@ static int imap_client_create(struct cli
+               return -1;
+       }
+ 
+-      struct imap_parser_params params = {
++      const struct imap_parser_params params = {
+               .list_count_limit = IMAP_LOGIN_LIST_COUNT_LIMIT,
+       };
+       imap_client->parser =
+@@ -467,7 +467,7 @@ static void imap_client_starttls(struct
+       struct imap_client *imap_client =
+               container_of(client, struct imap_client, common);
+ 
+-      struct imap_parser_params params = {
++      const struct imap_parser_params params = {
+               .list_count_limit = IMAP_LOGIN_LIST_COUNT_LIMIT,
+       };
+       imap_parser_unref(&imap_client->parser);
+--- dovecot-2.4.1+dfsg1.orig/src/imap-login/imap-login-cmd-id.c
++++ dovecot-2.4.1+dfsg1/src/imap-login/imap-login-cmd-id.c
+@@ -345,7 +345,7 @@ int cmd_id(struct imap_client *client)
+               id->params = p_new(param_pool, struct imap_id_params, 1);
+               id->params->pool = param_pool;
+ 
+-              struct imap_parser_params params = {
++              const struct imap_parser_params params = {
+                       .list_count_limit = IMAP_LOGIN_LIST_COUNT_LIMIT,
+               };
+               id->parser = imap_parser_create(client->common.input,
+--- dovecot-2.4.1+dfsg1.orig/src/lib-imap/test-imap-parser.c
++++ dovecot-2.4.1+dfsg1/src/lib-imap/test-imap-parser.c
+@@ -95,7 +95,7 @@ static void test_imap_parser_list_limit(
+       const struct imap_arg *args;
+ 
+       test_begin("imap parser list limit");
+-      struct imap_parser_params params = {
++      const struct imap_parser_params params = {
+               .list_count_limit = 2,
+       };
+ 
diff --git a/patches/CVE-2026-27858.patch b/patches/CVE-2026-27858.patch
new file mode 100644 (file)
index 0000000..0df753d
--- /dev/null
@@ -0,0 +1,31 @@
+From fcb339d39bfb19abf1bb88711112f49c1c589b1a Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Mon, 2 Mar 2026 14:40:57 +0200
+Subject: [PATCH 2/2] managesieve-login: Verify AUTHENTICATE initial response
+ size isn't too large
+
+This prevents DoSing the managesieve-login by sending an excessively large
+initial response size, which causes a huge memory allocation.
+---
+ src/managesieve-login/client-authenticate.c | 5 +++++
+ 1 file changed, 5 insertions(+)
+
+diff --git a/pigeonhole/src/managesieve-login/client-authenticate.c b/pigeonhole/src/managesieve-login/client-authenticate.c
+index 822dae6a..5aeccd35 100644
+--- a/pigeonhole/src/managesieve-login/client-authenticate.c
++++ b/pigeonhole/src/managesieve-login/client-authenticate.c
+@@ -196,6 +196,11 @@ managesieve_client_auth_read_response(struct managesieve_client *msieve_client,
+               if (i_stream_get_size(msieve_client->auth_response_input,
+                                     FALSE, &resp_size) <= 0)
+                       resp_size = 0;
++              else if (resp_size > LOGIN_MAX_AUTH_BUF_SIZE) {
++                      client_destroy(client,
++                                     "Authentication response too large");
++                      return -1;
++              }
+ 
+               if (client->auth_response == NULL) {
+                       client->auth_response =
+-- 
+2.39.5
+
diff --git a/patches/CVE-2026-27859.patch b/patches/CVE-2026-27859.patch
new file mode 100644 (file)
index 0000000..1fa2ab3
--- /dev/null
@@ -0,0 +1,100 @@
+From 4041a0cb7bbaa6faf38670ab92afc1039dd0d0d5 Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Tue, 24 Feb 2026 13:11:14 +0200
+Subject: [PATCH 03/24] lib-mail: Limit the number of RFC2231 parameters that
+ can be parsed
+
+This avoids excessive CPU usage especially in result_append().
+---
+ src/lib-mail/rfc2231-parser.c      |  4 +++-
+ src/lib-mail/rfc822-parser.h       |  5 +++++
+ src/lib-mail/test-rfc2231-parser.c | 30 ++++++++++++++++++++++++++++++
+ 3 files changed, 38 insertions(+), 1 deletion(-)
+
+--- dovecot-2.4.1+dfsg1.orig/src/lib-mail/rfc2231-parser.c
++++ dovecot-2.4.1+dfsg1/src/lib-mail/rfc2231-parser.c
+@@ -203,7 +203,7 @@ int rfc2231_parse(struct rfc822_parser_c
+       struct rfc2231_parameter rfc2231_param;
+       const char *key, *p, *p2;
+       string_t *str;
+-      unsigned int i, j, count, next, next_idx;
++      unsigned int i, j, count, next, next_idx, params_count = 0;
+       bool ok, broken = FALSE;
+       const char *prev_replacement_str;
+       int ret;
+@@ -221,6 +221,8 @@ int rfc2231_parse(struct rfc822_parser_c
+       t_array_init(&rfc2231_params_arr, 8);
+       str = t_str_new(64);
+       while ((ret = rfc822_parse_content_param(ctx, &key, str)) != 0) {
++              if (++params_count > RFC2231_MAX_PARAMS)
++                      break;
+               if (ret < 0) {
+                       /* try to continue anyway.. */
+                       broken = TRUE;
+--- dovecot-2.4.1+dfsg1.orig/src/lib-mail/rfc822-parser.h
++++ dovecot-2.4.1+dfsg1/src/lib-mail/rfc822-parser.h
+@@ -3,6 +3,11 @@
+ 
+ #include "unichar.h"
+ 
++/* Maximum number of parameters to parse. After this the rest of the parameters
++   are skipped. This is to avoid excessive CPU usage that can be caused by
++   merging of these parameters. */
++#define RFC2231_MAX_PARAMS 128
++
+ /* This can be used as a common NUL replacement character */
+ #define RFC822_NUL_REPLACEMENT_STR UNICODE_REPLACEMENT_CHAR_UTF8
+ 
+--- dovecot-2.4.1+dfsg1.orig/src/lib-mail/test-rfc2231-parser.c
++++ dovecot-2.4.1+dfsg1/src/lib-mail/test-rfc2231-parser.c
+@@ -1,6 +1,7 @@
+ /* Copyright (c) 2007-2018 Dovecot authors, see the included COPYING file */
+ 
+ #include "lib.h"
++#include "str.h"
+ #include "rfc822-parser.h"
+ #include "rfc2231-parser.h"
+ #include "test-common.h"
+@@ -249,6 +250,34 @@ static void test_rfc2231_parser_multi_en
+       run_test("rfc2231 parser invalid encoding", input, sizeof(input)-1, output);
+ }
+ 
++static void test_rfc2231_parser_limits(void)
++{
++      string_t *input = t_str_new(1024);
++
++      test_begin("rfc2231 parser limits");
++      str_append(input, "; ");
++      for (unsigned int i = 0; i < 1100; i++)
++              str_printfa(input, "a%u=b%u; ", i, i);
++      struct rfc822_parser_context parser;
++      const char *const *result;
++      rfc822_parser_init(&parser, str_data(input), str_len(input), NULL);
++      test_assert(rfc2231_parse(&parser, &result) == 0);
++
++      unsigned int count = str_array_length(result);
++      test_assert(count == RFC2231_MAX_PARAMS * 2);
++      for (unsigned int i = 0; i < count; i += 2) {
++              str_truncate(input, 0);
++              str_printfa(input, "a%u", i / 2);
++              test_assert_strcmp_idx(result[i], str_c(input), i);
++
++              str_truncate(input, 0);
++              str_printfa(input, "b%u", i / 2);
++              test_assert_strcmp_idx(result[i + 1], str_c(input), i);
++      }
++      rfc822_parser_deinit(&parser);
++      test_end();
++}
++
+ int main(void)
+ {
+       static void (*const test_functions[])(void) = {
+@@ -264,6 +293,7 @@ int main(void)
+               test_rfc2231_parser_encodings,
+               test_rfc2231_parser_utf8_encoding,
+               test_rfc2231_parser_multi_encodings,
++              test_rfc2231_parser_limits,
+               NULL
+       };
+       return test_run(test_functions);
diff --git a/patches/CVE-2026-33603.patch b/patches/CVE-2026-33603.patch
new file mode 100644 (file)
index 0000000..fb668a6
--- /dev/null
@@ -0,0 +1,36 @@
+From c1c53885bda550632b944dd305013cd010e0e058 Mon Sep 17 00:00:00 2001
+From: Aki Tuomi <aki.tuomi@open-xchange.com>
+Date: Wed, 8 Apr 2026 11:33:11 +0300
+Subject: [PATCH] login-common: Only accept base64 in sasl
+
+---
+ src/login-common/client-common-auth.c | 9 +++++++++
+ 1 file changed, 9 insertions(+)
+
+Index: dovecot/src/login-common/client-common-auth.c
+===================================================================
+--- dovecot.orig/src/login-common/client-common-auth.c
++++ dovecot/src/login-common/client-common-auth.c
+@@ -3,6 +3,7 @@
+ #include "hostpid.h"
+ #include "login-common.h"
+ #include "array.h"
++#include "base64.h"
+ #include "iostream.h"
+ #include "istream.h"
+ #include "ostream.h"
+@@ -865,6 +866,14 @@ void client_auth_respond(struct client *
+               return;
+       }
+ 
++      /* Only accept base64 */
++      for (size_t i = 0; response[i] != '\0'; i++) {
++              if (!base64_is_valid_char(response[i]) && response[i] != '=') {
++                      client_auth_fail(client, "Invalid base64 in response");
++                      return;
++              }
++      }
++
+       client->auth_client_continue_pending = FALSE;
+       client_set_auth_waiting(client);
+       sasl_server_auth_continue(client, response);
diff --git a/patches/CVE-2026-40016.patch b/patches/CVE-2026-40016.patch
new file mode 100644 (file)
index 0000000..b14198c
--- /dev/null
@@ -0,0 +1,255 @@
+From 5b0ed9d1034c023d3daf218b6b8656f0cdd383dc Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Sun, 19 Apr 2026 23:10:29 +0000
+Subject: [PATCH] lib-sieve: Enforce CPU time limit within :contains and
+ :matches matcher loops
+
+The naive O(N*M) substring search in mcht-contains.c and the naive find loop
+in mcht-matches.c can run for hours on a large value (e.g. a message body),
+completely bypassing sieve_max_cpu_time because that limit was only checked
+between bytecode operations.
+
+Expose the active CPU limit via sieve_runtime_cpu_limit_exceeded() and poll
+it every 4096 inner iterations. When the limit is hit the match returns
+SIEVE_EXEC_RESOURCE_LIMIT, matching the existing behavior at the bytecode
+boundary. This is a minimal safety net ahead of switching the matchers to
+algorithms that do not require it.
+---
+ src/lib-sieve/mcht-contains.c     | 21 ++++++++++++
+ src/lib-sieve/mcht-matches.c      | 57 +++++++++++++++++++++++++++----
+ src/lib-sieve/sieve-interpreter.c | 19 +++++++++++
+ src/lib-sieve/sieve-interpreter.h | 12 +++++++
+ 4 files changed, 103 insertions(+), 6 deletions(-)
+
+Index: dovecot/pigeonhole/src/lib-sieve/mcht-contains.c
+===================================================================
+--- dovecot.orig/pigeonhole/src/lib-sieve/mcht-contains.c
++++ dovecot/pigeonhole/src/lib-sieve/mcht-contains.c
+@@ -8,6 +8,7 @@
+ 
+ #include "sieve-match-types.h"
+ #include "sieve-comparators.h"
++#include "sieve-interpreter.h"
+ #include "sieve-match.h"
+ 
+ #include <string.h>
+@@ -38,7 +39,14 @@ const struct sieve_match_type_def contai
+ 
+ /* FIXME: Naive substring match implementation. Should switch to more
+  * efficient algorithm if large values need to be searched (e.g. message body).
++ *
++ * The inner loop polls the interpreter CPU time limit periodically so that a
++ * single O(N*M) match on a large value cannot run for many times the
++ * configured sieve_max_cpu_time (which is otherwise only checked between
++ * bytecode operations).
+  */
++#define SIEVE_CONTAINS_CPU_CHECK_INTERVAL 4096
++
+ static int mcht_contains_match_key
+ (struct sieve_match_context *mctx, const char *val, size_t val_size,
+       const char *key, size_t key_size)
+@@ -48,6 +56,7 @@ static int mcht_contains_match_key
+       const char *kend = (const char *) key + key_size;
+       const char *vp = val;
+       const char *kp = key;
++      unsigned int counter = 0;
+ 
+       if ( val_size == 0 )
+               return ( key_size == 0 ? 1 : 0 );
+@@ -58,6 +67,18 @@ static int mcht_contains_match_key
+       while ( (vp < vend) && (kp < kend) ) {
+               if ( !cmp->def->char_match(cmp, &vp, vend, &kp, kend) )
+                       vp++;
++
++              if ( ++counter >= SIEVE_CONTAINS_CPU_CHECK_INTERVAL ) {
++                      counter = 0;
++                      if ( sieve_runtime_cpu_limit_exceeded(mctx->runenv) ) {
++                              sieve_runtime_error(
++                                      mctx->runenv, NULL,
++                                      "execution exceeded CPU time limit");
++                              mctx->exec_status =
++                                      SIEVE_EXEC_RESOURCE_LIMIT;
++                              return -1;
++                      }
++              }
+       }
+ 
+       return ( kp == kend ? 1 : 0 );
+Index: dovecot/pigeonhole/src/lib-sieve/mcht-matches.c
+===================================================================
+--- dovecot.orig/pigeonhole/src/lib-sieve/mcht-matches.c
++++ dovecot/pigeonhole/src/lib-sieve/mcht-matches.c
+@@ -9,6 +9,7 @@
+ 
+ #include "sieve-match-types.h"
+ #include "sieve-comparators.h"
++#include "sieve-interpreter.h"
+ #include "sieve-match.h"
+ 
+ #include <string.h>
+@@ -46,16 +47,38 @@ const struct sieve_match_type_def matche
+ #endif
+ 
+ /* FIXME: Naive implementation, substitute this with dovecot src/lib/str-find.c
++ *
++ * The inner loop polls the interpreter CPU time limit periodically so that a
++ * single O(N*M) search on a large value cannot run for many times the
++ * configured sieve_max_cpu_time. Returns 1 on match, 0 on exhaustion, or -1
++ * when the CPU time limit was exceeded (mctx->exec_status is set).
+  */
+-static inline bool _string_find(const struct sieve_comparator *cmp,
+-      const char **valp, const char *vend, const char **keyp, const char *kend)
++#define SIEVE_MATCHES_CPU_CHECK_INTERVAL 4096
++
++static int
++_string_find(struct sieve_match_context *mctx,
++           const struct sieve_comparator *cmp,
++           const char **valp, const char *vend,
++           const char **keyp, const char *kend,
++           unsigned int *counter)
+ {
+       while ( (*valp < vend) && (*keyp < kend) ) {
+               if ( !cmp->def->char_match(cmp, valp, vend, keyp, kend) )
+                       (*valp)++;
++              if (++(*counter) >= SIEVE_MATCHES_CPU_CHECK_INTERVAL) {
++                      *counter = 0;
++                      if (sieve_runtime_cpu_limit_exceeded(mctx->runenv)) {
++                              sieve_runtime_error(
++                                      mctx->runenv, NULL,
++                                      "execution exceeded CPU time limit");
++                              mctx->exec_status =
++                                      SIEVE_EXEC_RESOURCE_LIMIT;
++                              return -1;
++                      }
++              }
+       }
+ 
+-      return (*keyp == kend);
++      return (*keyp == kend ? 1 : 0);
+ }
+ 
+ static char _scan_key_section
+@@ -93,6 +116,7 @@ static int mcht_matches_match_key
+       char wcard = '\0';      /* Current wildcard */
+       char next_wcard = '\0'; /* Next  widlcard */
+       unsigned int key_offset = 0;
++      unsigned int counter = 0;
+ 
+       if ( cmp->def == NULL || cmp->def->char_match == NULL )
+               return 0;
+@@ -134,6 +158,19 @@ static int mcht_matches_match_key
+       while (kp < kend && vp < vend ) {
+               const char *needle, *nend;
+ 
++              if (++counter >= SIEVE_MATCHES_CPU_CHECK_INTERVAL) {
++                      counter = 0;
++                      if (sieve_runtime_cpu_limit_exceeded(mctx->runenv)) {
++                              sieve_runtime_error(
++                                      mctx->runenv, NULL,
++                                      "execution exceeded CPU time limit");
++                              mctx->exec_status =
++                                      SIEVE_EXEC_RESOURCE_LIMIT;
++                              sieve_match_values_abort(&mvalues);
++                              return -1;
++                      }
++              }
++
+               if ( !backtrack ) {
+                       /* Search the next '*' wildcard in the key string */
+ 
+@@ -268,10 +305,20 @@ static int mcht_matches_match_key
+ 
+                               /* Match may happen at any offset (>= key offset): find substring */
+                               vp += key_offset;
+-                              if ( (vp >= vend) || !_string_find(cmp, &vp, vend, &needle, nend) ) {
++                              if (vp >= vend) {
+                                       debug_printf("  failed to find needle at an offset\n");
+                                       break;
+                               }
++                              int fres = _string_find(mctx, cmp, &vp, vend,
++                                                      &needle, nend, &counter);
++                              if (fres < 0) {
++                                      sieve_match_values_abort(&mvalues);
++                                      return -1;
++                              }
++                              if (fres == 0) {
++                                      debug_printf("  failed to find needle at an offset\n");
++                                      break;
++                              }
+ 
+                               prv = vp - str_len(section);
+                               prk = kp;
+Index: dovecot/pigeonhole/src/lib-sieve/sieve-interpreter.c
+===================================================================
+--- dovecot.orig/pigeonhole/src/lib-sieve/sieve-interpreter.c
++++ dovecot/pigeonhole/src/lib-sieve/sieve-interpreter.c
+@@ -85,6 +85,13 @@ struct sieve_interpreter {
+       struct sieve_runtime_trace trace;
+       struct sieve_resource_usage rusage;
+ 
++      /* CPU time limit for the current sieve_interpreter_continue() call;
++         NULL when no limit is configured or not currently executing. Exposed
++         via sieve_runtime_cpu_limit_exceeded() so long-running runtime code
++         can enforce the limit without waiting for the next bytecode
++         boundary. */
++      struct cpu_limit *climit;
++
+       /* Current operation */
+       struct sieve_operation oprtn;
+ 
+@@ -362,6 +369,15 @@ sieve_interpreter_svinst(struct sieve_in
+       return interp->runenv.exec_env->svinst;
+ }
+ 
++bool sieve_runtime_cpu_limit_exceeded(const struct sieve_runtime_env *renv)
++{
++      struct sieve_interpreter *interp = renv->interp;
++
++      if (interp->climit == NULL)
++              return FALSE;
++      return cpu_limit_exceeded(interp->climit);
++}
++
+ /* Do not use this function for normal sieve extensions. This is intended for
+  * the testsuite only.
+  */
+@@ -939,6 +955,7 @@ int sieve_interpreter_continue(struct si
+               climit = cpu_limit_init(svinst->set->max_cpu_time,
+                                       CPU_LIMIT_TYPE_USER);
+       }
++      interp->climit = climit;
+ 
+       while (ret == SIEVE_EXEC_OK && !interp->interrupted &&
+              *address < sieve_binary_block_get_size(renv->sblock)) {
+@@ -959,6 +976,8 @@ int sieve_interpreter_continue(struct si
+               ret = sieve_interpreter_operation_execute(interp);
+       }
+ 
++      interp->climit = NULL;
++
+       if (climit != NULL) {
+               sieve_resource_usage_init(&rusage);
+               rusage.cpu_time_msecs =
+Index: dovecot/pigeonhole/src/lib-sieve/sieve-interpreter.h
+===================================================================
+--- dovecot.orig/pigeonhole/src/lib-sieve/sieve-interpreter.h
++++ dovecot/pigeonhole/src/lib-sieve/sieve-interpreter.h
+@@ -165,6 +165,18 @@ int sieve_interpreter_run(struct sieve_i
+                         struct sieve_result *result);
+ 
+ /*
++ * CPU limit
++ */
++
++/* Returns TRUE if the current interpreter execution has exceeded its CPU
++   time limit (sieve_max_cpu_time). Callable from within long-running runtime
++   code (e.g. matcher inner loops) so that limit enforcement is not deferred
++   until the next bytecode boundary. Returns FALSE if no limit is active or
++   no execution is currently in progress. Cheap: does not call getrusage()
++   on each invocation. */
++bool sieve_runtime_cpu_limit_exceeded(const struct sieve_runtime_env *renv);
++
++/*
+  * Error handling
+  */
+ 
diff --git a/patches/CVE-2026-40020-1.patch b/patches/CVE-2026-40020-1.patch
new file mode 100644 (file)
index 0000000..c9b2c2e
--- /dev/null
@@ -0,0 +1,78 @@
+From b7daa4104ff064c1fb549540cc9d96c2d9e2509c Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Wed, 22 Apr 2026 15:43:58 +0300
+Subject: [PATCH 1/3] acl: Add acl_id_is_valid()
+
+Returns TRUE if the ACL identifier string is at most ACL_ID_MAX_LEN
+(1024) bytes long, contains no control characters and is valid UTF-8.
+---
+ src/plugins/acl/acl-rights.c | 16 ++++++++++++++++
+ src/plugins/acl/acl-rights.h |  6 ++++++
+ 2 files changed, 22 insertions(+)
+
+diff --git a/src/plugins/acl/acl-rights.c b/src/plugins/acl/acl-rights.c
+index 654ee00aef..5729f8b52a 100644
+--- a/src/plugins/acl/acl-rights.c
++++ b/src/plugins/acl/acl-rights.c
+@@ -3,11 +3,14 @@
+ #include "lib.h"
+ #include "array.h"
+ #include "str.h"
++#include "unichar.h"
+ /* <settings checks> */
+ #include "strescape.h"
+ /* </settings checks> */
+ #include "acl-api-private.h"
+ 
++#include <ctype.h>
++
+ /* <settings checks> */
+ const struct acl_letter_map acl_letter_map[] = {
+       { 'l', MAIL_ACL_LOOKUP },
+@@ -44,6 +47,19 @@ static_assert(N_ELEMENTS(acl_letter_map) == N_ELEMENTS(all_mailbox_rights),
+ 
+ /* </settings checks> */
+ 
++bool acl_id_is_valid(const char *id)
++{
++      size_t len = strlen(id);
++
++      if (len > ACL_ID_MAX_LEN)
++              return FALSE;
++      for (size_t i = 0; i < len; i++) {
++              if (i_iscntrl(id[i]))
++                      return FALSE;
++      }
++      return uni_utf8_data_is_valid((const unsigned char *)id, len);
++}
++
+ void acl_rights_write_id(string_t *dest, const struct acl_rights *right)
+ {
+       switch (right->id_type) {
+diff --git a/src/plugins/acl/acl-rights.h b/src/plugins/acl/acl-rights.h
+index 88ef73e3b4..32163e7c3d 100644
+--- a/src/plugins/acl/acl-rights.h
++++ b/src/plugins/acl/acl-rights.h
+@@ -34,6 +34,8 @@
+ #define ACL_ID_NAME_GROUP_PREFIX "group="
+ #define ACL_ID_NAME_GROUP_OVERRIDE_PREFIX "group-override="
+ 
++#define ACL_ID_MAX_LEN 1024
++
+ struct acl_letter_map {
+       const char letter;
+       const char *name;
+@@ -104,6 +106,10 @@ struct acl_rights_update {
+       time_t last_change;
+ };
+ 
++/* Returns TRUE if the ACL identifier string is valid: no longer than
++   ACL_ID_MAX_LEN bytes, no control characters and valid UTF-8. */
++bool acl_id_is_valid(const char *id);
++
+ /* Returns the canonical ID for the right. */
+ const char *acl_rights_get_id(const struct acl_rights *right);
+ 
+-- 
+2.47.3
+
diff --git a/patches/CVE-2026-40020-2.patch b/patches/CVE-2026-40020-2.patch
new file mode 100644 (file)
index 0000000..62f437f
--- /dev/null
@@ -0,0 +1,28 @@
+From 20b48c3db5fed7ccaa8e0a4c10ca54f6dc36a63d Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Wed, 22 Apr 2026 15:44:24 +0300
+Subject: [PATCH 2/3] imap-acl: Fail if ACL identifier is invalid
+
+Reject invalid identifiers early in imap_acl_identifier_parse() using
+acl_id_is_valid(). This prevents CR/LF injection and rejects identifiers
+that are too long, contain control characters or are not valid UTF-8.
+---
+ src/plugins/imap-acl/imap-acl-plugin.c | 5 +++++
+ 1 file changed, 5 insertions(+)
+
+Index: dovecot/src/plugins/imap-acl/imap-acl-plugin.c
+===================================================================
+--- dovecot.orig/src/plugins/imap-acl/imap-acl-plugin.c
++++ dovecot/src/plugins/imap-acl/imap-acl-plugin.c
+@@ -872,6 +872,11 @@ imap_acl_identifier_parse(struct client_
+       allow_anyone = set->allow_anyone;
+       settings_free(set);
+ 
++      if (!acl_id_is_valid(id)) {
++              *client_error_r = "Invalid identifier";
++              return -1;
++      }
++
+       if (str_begins_with(id, IMAP_ACL_GLOBAL_PREFIX)) {
+               *client_error_r = t_strdup_printf(
+                       "Global ACLs can't be modified: %s", id);
diff --git a/patches/CVE-2026-40020-3.patch b/patches/CVE-2026-40020-3.patch
new file mode 100644 (file)
index 0000000..03fabcc
--- /dev/null
@@ -0,0 +1,23 @@
+From 1cf6ad1a119e5dace816e401e73ba6cc11d1472e Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Wed, 22 Apr 2026 15:45:00 +0300
+Subject: [PATCH 3/3] acl: Assert-crash if ACL identifier is invalid before
+ writing it
+
+It should have been checked earlier already.
+---
+ src/plugins/acl/acl-backend-vfile-update.c | 1 +
+ 1 file changed, 1 insertion(+)
+
+Index: dovecot/src/plugins/acl/acl-backend-vfile-update.c
+===================================================================
+--- dovecot.orig/src/plugins/acl/acl-backend-vfile-update.c
++++ dovecot/src/plugins/acl/acl-backend-vfile-update.c
+@@ -119,6 +119,7 @@ vfile_write_right(string_t *dest, const
+       if (neg) str_append_c(dest,'-');
+       acl_rights_write_id(dest, right);
+ 
++      i_assert(acl_id_is_valid(str_c(dest)));
+       if (strchr(str_c(dest), ' ') != NULL) T_BEGIN {
+               /* need to escape it */
+               const char *escaped = t_strdup(str_escape(str_c(dest)));
diff --git a/patches/CVE-2026-42006.patch b/patches/CVE-2026-42006.patch
new file mode 100644 (file)
index 0000000..9d9c4e0
--- /dev/null
@@ -0,0 +1,101 @@
+From da1438c76b797f055d4ad7f0eaa17e5e29ca31ee Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Mon, 27 Apr 2026 17:40:46 +0300
+Subject: [PATCH] lib-imap: Fix imap_parser_params.list_count_limit to actually
+ work
+
+The previous fix in d0f67b52914565a35f3817335ab9633cb291513c was
+accidentally limiting the number of ')', not the number of '('.
+---
+ src/lib-imap/imap-parser.c      | 19 +++++++++++--------
+ src/lib-imap/test-imap-parser.c | 14 ++++++++++++--
+ 2 files changed, 23 insertions(+), 10 deletions(-)
+
+diff --git a/src/lib-imap/imap-parser.c b/src/lib-imap/imap-parser.c
+index 6212aed33d..b1df3d7b78 100644
+--- a/src/lib-imap/imap-parser.c
++++ b/src/lib-imap/imap-parser.c
+@@ -191,8 +191,15 @@ static struct imap_arg *imap_arg_create(struct imap_parser *parser)
+       return arg;
+ }
+ 
+-static void imap_parser_open_list(struct imap_parser *parser)
++static bool imap_parser_open_list(struct imap_parser *parser)
+ {
++      if (parser->list_count >= parser->list_count_limit) {
++              parser->error_msg = "Too many '('";
++              parser->error = IMAP_PARSE_ERROR_BAD_SYNTAX;
++              return FALSE;
++      }
++      parser->list_count++;
++
+       parser->list_arg = imap_arg_create(parser);
+       parser->list_arg->type = IMAP_ARG_LIST;
+       p_array_init(&parser->list_arg->_data.list, parser->pool,
+@@ -200,6 +207,7 @@ static void imap_parser_open_list(struct imap_parser *parser)
+       parser->cur_list = &parser->list_arg->_data.list;
+ 
+       parser->cur_type = ARG_PARSE_NONE;
++      return TRUE;
+ }
+ 
+ static bool imap_parser_close_list(struct imap_parser *parser)
+@@ -217,12 +225,6 @@ static bool imap_parser_close_list(struct imap_parser *parser)
+               parser->error = IMAP_PARSE_ERROR_BAD_SYNTAX;
+               return FALSE;
+       }
+-      if (parser->list_count >= parser->list_count_limit) {
+-              parser->error_msg = "Too many '('";
+-              parser->error = IMAP_PARSE_ERROR_BAD_SYNTAX;
+-              return FALSE;
+-      }
+-      parser->list_count++;
+ 
+       arg = imap_arg_create(parser);
+       arg->type = IMAP_ARG_EOL;
+@@ -673,7 +675,8 @@ static bool imap_parser_read_arg(struct imap_parser *parser)
+                       parser->literal8 = FALSE;
+                       break;
+               case '(':
+-                      imap_parser_open_list(parser);
++                      if (!imap_parser_open_list(parser))
++                              return FALSE;
+                       if ((parser->flags & IMAP_PARSE_FLAG_STOP_AT_LIST) != 0) {
+                               i_stream_skip(parser->input, 1);
+                               return FALSE;
+diff --git a/src/lib-imap/test-imap-parser.c b/src/lib-imap/test-imap-parser.c
+index fb7c308a23..d509c8176d 100644
+--- a/src/lib-imap/test-imap-parser.c
++++ b/src/lib-imap/test-imap-parser.c
+@@ -85,9 +85,15 @@ static void test_imap_parser_list_limit(void)
+       struct {
+               const char *input;
+               int ret;
++              const char *error;
+       } tests[] = {
+-              { "(())\r\n", 1 },
+-              { "((()))\r\n", -1 },
++              { "(())\r\n", 1, NULL },
++              { "((\r\n", -1, "Missing ')'" },
++              { "(()\r\n", -1, "Missing ')'" },
++              { "(()))\r\n", -1, "Unexpected ')'" },
++              { "((()))\r\n", -1, "Too many '('" },
++              { "(({10}\r\n", -2, NULL },
++              { "((({10}\r\n", -1, "Too many '('" },
+       };
+       struct istream_chain *chain;
+       struct istream *chain_input;
+@@ -112,6 +118,10 @@ static void test_imap_parser_list_limit(void)
+                       (void)i_stream_read(chain_input);
+ 
+                       test_assert_cmp(imap_parser_read_args(parser, 0, 0, &args), ==, tests[i].ret);
++                      if (tests[i].ret == -1) {
++                              enum imap_parser_error err;
++                              test_assert_strcmp_idx(imap_parser_get_error(parser, &err), tests[i].error, i);
++                      }
+                       /* skip over CRLF */
+                       i_stream_skip(chain_input, i_stream_get_data_size(chain_input));
+ 
+-- 
+2.47.3
+
diff --git a/patches/Use-_FORTIFY_SOURCE-level-3.patch b/patches/Use-_FORTIFY_SOURCE-level-3.patch
new file mode 100644 (file)
index 0000000..5fba84a
--- /dev/null
@@ -0,0 +1,58 @@
+From: =?utf-8?q?Christian_G=C3=B6ttsche?= <cgzones@googlemail.com>
+Date: Thu, 22 Dec 2022 17:00:53 +0100
+Subject: Use _FORTIFY_SOURCE level 3
+
+Forwarded: not-needed
+---
+ m4/dovecot.m4            | 6 +++---
+ pigeonhole/m4/dovecot.m4 | 6 +++---
+ 2 files changed, 6 insertions(+), 6 deletions(-)
+
+Index: dovecot/m4/dovecot.m4
+===================================================================
+--- dovecot.orig/m4/dovecot.m4
++++ dovecot/m4/dovecot.m4
+@@ -9,7 +9,7 @@ dnl modifications, as long as this notic
+ # serial 42
+ 
+ dnl
+-dnl Check for support for D_FORTIFY_SOURCE=2
++dnl Check for support for D_FORTIFY_SOURCE=3
+ dnl
+ 
+ AC_DEFUN([AC_CC_D_FORTIFY_SOURCE],[
+@@ -17,8 +17,8 @@ AC_DEFUN([AC_CC_D_FORTIFY_SOURCE],[
+     AS_IF([test "$enable_hardening" = yes], [
+       case "$host" in
+         *)
+-          gl_COMPILER_OPTION_IF([-O2 -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=2], [
+-            CFLAGS="$CFLAGS -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=2"
++          gl_COMPILER_OPTION_IF([-O2 -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3], [
++            CFLAGS="$CFLAGS -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3"
+             ],
+             [],
+             [AC_LANG_PROGRAM()]
+Index: dovecot/pigeonhole/m4/dovecot.m4
+===================================================================
+--- dovecot.orig/pigeonhole/m4/dovecot.m4
++++ dovecot/pigeonhole/m4/dovecot.m4
+@@ -9,7 +9,7 @@ dnl modifications, as long as this notic
+ # serial 41
+ 
+ dnl
+-dnl Check for support for D_FORTIFY_SOURCE=2
++dnl Check for support for D_FORTIFY_SOURCE=3
+ dnl
+ 
+ AC_DEFUN([AC_CC_D_FORTIFY_SOURCE],[
+@@ -17,8 +17,8 @@ AC_DEFUN([AC_CC_D_FORTIFY_SOURCE],[
+     AS_IF([test "$enable_hardening" = yes], [
+       case "$host" in
+         *)
+-          gl_COMPILER_OPTION_IF([-O2 -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=2], [
+-            CFLAGS="$CFLAGS -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=2"
++          gl_COMPILER_OPTION_IF([-O2 -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3], [
++            CFLAGS="$CFLAGS -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3"
+             ],
+             [],
+             [AC_LANG_PROGRAM()]
diff --git a/patches/acl-Fix-crash-when-group-ACLs-are-used-but-user-s-ac.patch b/patches/acl-Fix-crash-when-group-ACLs-are-used-but-user-s-ac.patch
new file mode 100644 (file)
index 0000000..c1b0cfb
--- /dev/null
@@ -0,0 +1,28 @@
+From 003bf9a6959714e0f696f0015c8c712e89962b9b Mon Sep 17 00:00:00 2001
+From: Marco Bettini <marco.bettini@open-xchange.com>
+Date: Thu, 28 Aug 2025 15:09:56 +0000
+Subject: [PATCH] acl: Fix crash when group ACLs are used, but user's
+ acl_groups is empty
+Bug-Debian: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1129952
+
+---
+ src/plugins/acl/acl-backend.c | 3 ++-
+ 1 file changed, 2 insertions(+), 1 deletion(-)
+
+diff --git a/src/plugins/acl/acl-backend.c b/src/plugins/acl/acl-backend.c
+index 2ad901cd96..cc08d702dd 100644
+--- a/src/plugins/acl/acl-backend.c
++++ b/src/plugins/acl/acl-backend.c
+@@ -155,7 +155,8 @@ bool acl_backend_user_name_equals(struct acl_backend *backend,
+ bool acl_backend_user_is_in_group(struct acl_backend *backend,
+                                 const char *group_name)
+ {
+-      return array_bsearch(&backend->set->acl_groups, group_name,
++      return array_not_empty(&backend->set->acl_groups) &&
++             array_bsearch(&backend->set->acl_groups, group_name,
+                            search_strcmp) != NULL;
+ }
+ 
+-- 
+2.47.3
+
diff --git a/patches/auth__Terminate_properly_auth_oauth2_post_setting_defines.patch b/patches/auth__Terminate_properly_auth_oauth2_post_setting_defines.patch
new file mode 100644 (file)
index 0000000..3ec4715
--- /dev/null
@@ -0,0 +1,25 @@
+From e546239af593dfd0722bea9ff169b60006e219f9 Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Thu, 15 May 2025 13:06:56 +0300
+Subject: [PATCH] auth: Terminate properly auth_oauth2_post_setting_defines
+ list
+
+Fixes:
+Error: xoauth2: oauth2 failed: Local validation failed: auth_oauth2_fields settings: Failed to parse configuration: settings struct auth_oauth2_fields #1 key mismatch
+---
+ src/auth/db-oauth2.c | 2 ++
+ 1 file changed, 2 insertions(+)
+
+diff --git a/src/auth/db-oauth2.c b/src/auth/db-oauth2.c
+index bd807e30b55..230c6cf7678 100644
+--- a/src/auth/db-oauth2.c
++++ b/src/auth/db-oauth2.c
+@@ -134,6 +134,8 @@ const struct setting_parser_info auth_oauth2_setting_parser_info = {
+ static const struct setting_define auth_oauth2_post_setting_defines[] = {
+       { .type = SET_STRLIST, .key = "oauth2_fields",
+         .offset = offsetof(struct auth_oauth2_post_settings, fields) },
++
++      SETTING_DEFINE_LIST_END
+ };
+ 
+ static const struct auth_oauth2_post_settings auth_oauth2_post_default_settings = {
diff --git a/patches/auth__Use_AUTH_CACHE_KEY_USER_instead_of_per-database.patch b/patches/auth__Use_AUTH_CACHE_KEY_USER_instead_of_per-database.patch
new file mode 100644 (file)
index 0000000..926c1ec
--- /dev/null
@@ -0,0 +1,124 @@
+From a70ce7d3e2f983979e971414c5892c4e30197231 Mon Sep 17 00:00:00 2001
+From: Aki Tuomi <aki.tuomi@open-xchange.com>
+Date: Fri, 25 Jul 2025 08:16:52 +0300
+Subject: [PATCH] auth: Use AUTH_CACHE_KEY_USER instead of per-database
+ constants
+
+Fixes cache key issue where users would end up overwriting
+each other in cache due to cache key being essentially static
+string because we no longer support %u.
+
+Forgotten in 2e298e7ee98b6df61cf85117f000290d60a473b8
+---
+ src/auth/auth-settings.h  | 2 ++
+ src/auth/passdb-bsdauth.c | 4 +---
+ src/auth/passdb-oauth2.c  | 2 +-
+ src/auth/passdb-pam.c     | 3 ++-
+ src/auth/passdb-passwd.c  | 3 +--
+ src/auth/userdb-passwd.c  | 3 +--
+ 6 files changed, 8 insertions(+), 9 deletions(-)
+
+diff --git a/src/auth/auth-settings.h b/src/auth/auth-settings.h
+index 1d420eceaaf..90aba17ec38 100644
+--- a/src/auth/auth-settings.h
++++ b/src/auth/auth-settings.h
+@@ -1,6 +1,8 @@
+ #ifndef AUTH_SETTINGS_H
+ #define AUTH_SETTINGS_H
+ 
++#define AUTH_CACHE_KEY_USER "%{user}"
++
+ struct master_service;
+ struct master_service_settings_output;
+ 
+diff --git a/src/auth/passdb-bsdauth.c b/src/auth/passdb-bsdauth.c
+index 68292679b7f..1b86da4053c 100644
+--- a/src/auth/passdb-bsdauth.c
++++ b/src/auth/passdb-bsdauth.c
+@@ -14,8 +14,6 @@
+ #include <login_cap.h>
+ #include <bsd_auth.h>
+ 
+-#define BSDAUTH_CACHE_KEY "%u"
+-
+ struct passdb_bsdauth_settings {
+       pool_t pool;
+ };
+@@ -104,7 +102,7 @@ bsdauth_preinit(pool_t pool, struct event *event,
+                        &post_set, error_r) < 0)
+               return -1;
+       module->default_cache_key = auth_cache_parse_key_and_fields(
+-              pool, BSDAUTH_CACHE_KEY, &post_set->fields, "bsdauth");
++              pool, AUTH_CACHE_KEY_USER, &post_set->fields, "bsdauth");
+ 
+       settings_free(post_set);
+       *module_r = module;
+diff --git a/src/auth/passdb-oauth2.c b/src/auth/passdb-oauth2.c
+index 96d902d323d..91fed060183 100644
+--- a/src/auth/passdb-oauth2.c
++++ b/src/auth/passdb-oauth2.c
+@@ -53,7 +53,7 @@ oauth2_preinit(pool_t pool, struct event *event, struct passdb_module **module_r
+       if (db_oauth2_init(event, TRUE, &module->db, error_r) < 0)
+               return -1;
+       module->module.default_pass_scheme = "PLAIN";
+-      module->module.default_cache_key = "%u";
++      module->module.default_cache_key = AUTH_CACHE_KEY_USER;
+       *module_r = &module->module;
+       return 0;
+ }
+diff --git a/src/auth/passdb-pam.c b/src/auth/passdb-pam.c
+index 2acbceb80a3..fdf0f573ef4 100644
+--- a/src/auth/passdb-pam.c
++++ b/src/auth/passdb-pam.c
+@@ -415,7 +415,8 @@ static int pam_preinit(pool_t pool, struct event *event,
+       module = p_new(pool, struct pam_passdb_module, 1);
+       module->module.default_cache_key =
+               auth_cache_parse_key_and_fields(pool,
+-                                              t_strdup_printf("%%u/%s", set->service_name),
++                                              t_strdup_printf("%"AUTH_CACHE_KEY_USER"\t%s",
++                                                              set->service_name),
+                                               &post_set->fields, "pam");
+       module->requests_left = set->max_requests;
+       module->pam_setcred = set->setcred;
+diff --git a/src/auth/passdb-passwd.c b/src/auth/passdb-passwd.c
+index 13003151f9c..22e2eae7fa3 100644
+--- a/src/auth/passdb-passwd.c
++++ b/src/auth/passdb-passwd.c
+@@ -10,7 +10,6 @@
+ #include "safe-memset.h"
+ #include "ipwd.h"
+ 
+-#define PASSWD_CACHE_KEY "%u"
+ #define PASSWD_PASS_SCHEME "CRYPT"
+ 
+ #undef DEF
+@@ -142,7 +141,7 @@ static int passwd_preinit(pool_t pool, struct event *event,
+                        &post_set, error_r) < 0)
+               return -1;
+       module->default_cache_key = auth_cache_parse_key_and_fields(pool,
+-                                                                  PASSWD_CACHE_KEY,
++                                                                  AUTH_CACHE_KEY_USER,
+                                                                   &post_set->fields,
+                                                                   "passwd");
+       settings_free(post_set);
+diff --git a/src/auth/userdb-passwd.c b/src/auth/userdb-passwd.c
+index 5241129a0cc..14cf90a6d65 100644
+--- a/src/auth/userdb-passwd.c
++++ b/src/auth/userdb-passwd.c
+@@ -9,7 +9,6 @@
+ #include "ipwd.h"
+ #include "time-util.h"
+ 
+-#define USER_CACHE_KEY "%u"
+ #define PASSWD_SLOW_WARN_MSECS (10*1000)
+ #define PASSWD_SLOW_MASTER_WARN_MSECS 50
+ #define PASSDB_SLOW_MASTER_WARN_COUNT_INTERVAL 100
+@@ -225,7 +224,7 @@ static int passwd_preinit(pool_t pool, struct event *event ATTR_UNUSED,
+       struct passwd_userdb_module *module =
+               p_new(pool, struct passwd_userdb_module, 1);
+ 
+-      module->module.default_cache_key = USER_CACHE_KEY;
++      module->module.default_cache_key = AUTH_CACHE_KEY_USER;
+       *module_r = &module->module;
+       return 0;
+ }
diff --git a/patches/bug1104549-gssapi-regression.patch b/patches/bug1104549-gssapi-regression.patch
new file mode 100644 (file)
index 0000000..d95115c
--- /dev/null
@@ -0,0 +1,21 @@
+Description: Fix GSSAPI regression
+ Dovecot 2.4 introduced a regression that broke GSSAPI authentication for
+ some clients.  This patch contains a fix provided by the upstream maintainers.
+Origin: https://dovecot.org/mailman3/archives/list/dovecot@dovecot.org/message/O54EAGLIXXHMOH7BQCCKHHB3Z32HDWVR/
+Bug-Debian: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1104549
+Last-Update: 2025-05-02
+---
+This patch header follows DEP-3: http://dep.debian.net/deps/dep3/
+Index: dovecot/src/auth/mech-gssapi.c
+===================================================================
+--- dovecot.orig/src/auth/mech-gssapi.c
++++ dovecot/src/auth/mech-gssapi.c
+@@ -672,7 +672,7 @@ mech_gssapi_auth_initial(struct auth_req
+ 
+       if (data_size == 0) {
+               /* The client should go first */
+-              auth_request_handler_reply_continue(request, NULL, 0);
++              auth_request_handler_reply_continue(request, uchar_empty_ptr, 0);
+       } else {
+               mech_gssapi_auth_continue(request, data, data_size);
+       }
diff --git a/patches/bug1106784_Fix-LDAP-SASL-auth-support.patch b/patches/bug1106784_Fix-LDAP-SASL-auth-support.patch
new file mode 100644 (file)
index 0000000..d032ccd
--- /dev/null
@@ -0,0 +1,39 @@
+From 09cc148e4bbbae91514b6ff58bd71c15f95e4e8a Mon Sep 17 00:00:00 2001
+From: Jakob Haufe <sur5r@sur5r.net>
+Date: Sun, 25 May 2025 15:04:50 +0200
+Subject: [PATCH] Fix LDAP SASL auth support
+
+961275fdb54878fdfa4ee1b9f1a4f00e82bf4a83 moved code without creating a
+way to have HAVE_LDAP_SASL defined there.
+
+Copy the preprocessor block from src/auth/db-ldap.c to fix this.
+---
+ src/auth/db-ldap-settings.c | 12 ++++++++++++
+ 1 file changed, 12 insertions(+)
+
+diff --git a/src/auth/db-ldap-settings.c b/src/auth/db-ldap-settings.c
+index 6c8feb334e..9245fcabd2 100644
+--- a/src/auth/db-ldap-settings.c
++++ b/src/auth/db-ldap-settings.c
+@@ -137,6 +137,18 @@ const struct setting_parser_info ldap_post_setting_parser_info = {
+ 
+ /* <settings checks> */
+ 
++#define HAVE_LDAP_SASL
++#ifdef HAVE_SASL_SASL_H
++#  include <sasl/sasl.h>
++#elif defined (HAVE_SASL_H)
++#  include <sasl.h>
++#else
++#  undef HAVE_LDAP_SASL
++#endif
++#if !defined(SASL_VERSION_MAJOR) || SASL_VERSION_MAJOR < 2
++#  undef HAVE_LDAP_SASL
++#endif
++
+ static int ldap_parse_deref(const char *str, int *ref_r)
+ {
+       if (strcasecmp(str, "never") == 0)
+-- 
+2.47.2
+
diff --git a/patches/bug1121000_dovecot-ldap_Crash_if_iterate_filter_is_set_but_iterate_fields_is_not_set.patch b/patches/bug1121000_dovecot-ldap_Crash_if_iterate_filter_is_set_but_iterate_fields_is_not_set.patch
new file mode 100644 (file)
index 0000000..776e669
--- /dev/null
@@ -0,0 +1,28 @@
+From 576a2f52bff4c13971d9e6d1172857a4f18ddd14 Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Thu, 6 Nov 2025 14:52:37 +0200
+Subject: [PATCH] auth: ldap - Fix crash if users are iterated, but
+ userdb_ldap_iterate_fields is not set
+Bug-Debian: https://bugs.debian.org/1121000
+---
+ src/auth/userdb-ldap.c | 7 +++++--
+ 1 file changed, 5 insertions(+), 2 deletions(-)
+
+diff --git a/src/auth/userdb-ldap.c b/src/auth/userdb-ldap.c
+index 7d6d2bc38a..7aad2db74f 100644
+--- a/src/auth/userdb-ldap.c
++++ b/src/auth/userdb-ldap.c
+@@ -201,8 +201,11 @@ static void userdb_ldap_iterate_callback(struct ldap_connection *conn,
+                        &set, &error) < 0) {
+               e_error(event, "%s", error);
+               ctx->ctx.failed = TRUE;
+-      }
+-      else {
++      } else if (!array_is_created(&set->iterate_fields)) {
++              e_error(event, "iterate: No userdb_ldap_iterate_fields specified");
++              ctx->ctx.failed = TRUE;
++              settings_free(set);
++      } else {
+               unsigned int count;
+               const char *const *items = array_get(&set->iterate_fields, &count);
+               for (unsigned int ndx = 0; ndx < count - 1;) {
diff --git a/patches/fit-32-bit-test-integers.patch b/patches/fit-32-bit-test-integers.patch
new file mode 100644 (file)
index 0000000..3238f76
--- /dev/null
@@ -0,0 +1,61 @@
+Index: dovecot/src/lib/test-event-filter.c
+===================================================================
+--- dovecot.orig/src/lib/test-event-filter.c
++++ dovecot/src/lib/test-event-filter.c
+@@ -1003,32 +1003,32 @@ static void test_event_filter_interval_v
+               { "field > 1mins", 60 * 1000 * 1000, FALSE },
+               { "field < 1mins", 60 * 1000 * 1000, FALSE },
+ 
+-              { "field = 1hours", 60L * 60 * 1000 * 1000, TRUE },
+-              { "field = 1h", 60L * 60 * 1000 * 1000, TRUE },
+-              { "field = 3600000000", 60L * 60 * 1000 * 1000, TRUE },
+-              { "field >= 1hours", 60L * 60 * 1000 * 1000, TRUE },
+-              { "field <= 1hours", 60L * 60 * 1000 * 1000, TRUE },
+-              { "field > 1mins", 60L * 60 * 1000 * 1000, TRUE },
+-              { "field > 1hours", 60L * 60 * 1000 * 1000, FALSE },
+-              { "field < 1hours", 60L * 60 * 1000 * 1000, FALSE },
++              { "field = 1hours", 60LL * 60 * 1000 * 1000, TRUE },
++              { "field = 1h", 60LL * 60 * 1000 * 1000, TRUE },
++              { "field = 3600000000", 60LL * 60 * 1000 * 1000, TRUE },
++              { "field >= 1hours", 60LL * 60 * 1000 * 1000, TRUE },
++              { "field <= 1hours", 60LL * 60 * 1000 * 1000, TRUE },
++              { "field > 1mins", 60LL * 60 * 1000 * 1000, TRUE },
++              { "field > 1hours", 60LL * 60 * 1000 * 1000, FALSE },
++              { "field < 1hours", 60LL * 60 * 1000 * 1000, FALSE },
+ 
+-              { "field = 1days", 24L * 60 * 60 * 1000 * 1000, TRUE },
+-              { "field = 1d", 24L * 60 * 60 * 1000 * 1000, TRUE },
+-              { "field = 86400000000", 24L * 60 * 60 * 1000 * 1000, TRUE },
+-              { "field >= 1days", 24L * 60 * 60 * 1000 * 1000, TRUE },
+-              { "field <= 1days", 24L * 60 * 60 * 1000 * 1000, TRUE },
+-              { "field > 1hours", 24L * 60 * 60 * 1000 * 1000, TRUE },
+-              { "field > 1days", 24L * 60 * 60 * 1000 * 1000, FALSE },
+-              { "field < 1days", 24L * 60 * 60 * 1000 * 1000, FALSE },
++              { "field = 1days", 24LL * 60 * 60 * 1000 * 1000, TRUE },
++              { "field = 1d", 24LL * 60 * 60 * 1000 * 1000, TRUE },
++              { "field = 86400000000", 24LL * 60 * 60 * 1000 * 1000, TRUE },
++              { "field >= 1days", 24LL * 60 * 60 * 1000 * 1000, TRUE },
++              { "field <= 1days", 24LL * 60 * 60 * 1000 * 1000, TRUE },
++              { "field > 1hours", 24LL * 60 * 60 * 1000 * 1000, TRUE },
++              { "field > 1days", 24LL * 60 * 60 * 1000 * 1000, FALSE },
++              { "field < 1days", 24LL * 60 * 60 * 1000 * 1000, FALSE },
+ 
+-              { "field = 1weeks", 7L * 24 * 60 * 60 * 1000 * 1000, TRUE },
+-              { "field = 1w", 7L * 24 * 60 * 60 * 1000 * 1000, TRUE },
+-              { "field = 604800000000", 7L * 24 * 60 * 60 * 1000 * 1000, TRUE },
+-              { "field >= 1weeks", 7L * 24 * 60 * 60 * 1000 * 1000, TRUE },
+-              { "field <= 1weeks", 7L * 24 * 60 * 60 * 1000 * 1000, TRUE },
+-              { "field > 1days", 7L * 24 * 60 * 60 * 1000 * 1000, TRUE },
+-              { "field > 1weeks", 7L * 24 * 60 * 60 * 1000 * 1000, FALSE },
+-              { "field < 1weeks", 7L * 24 * 60 * 60 * 1000 * 1000, FALSE },
++              { "field = 1weeks", 7LL * 24 * 60 * 60 * 1000 * 1000, TRUE },
++              { "field = 1w", 7LL * 24 * 60 * 60 * 1000 * 1000, TRUE },
++              { "field = 604800000000", 7LL * 24 * 60 * 60 * 1000 * 1000, TRUE },
++              { "field >= 1weeks", 7LL * 24 * 60 * 60 * 1000 * 1000, TRUE },
++              { "field <= 1weeks", 7LL * 24 * 60 * 60 * 1000 * 1000, TRUE },
++              { "field > 1days", 7LL * 24 * 60 * 60 * 1000 * 1000, TRUE },
++              { "field > 1weeks", 7LL * 24 * 60 * 60 * 1000 * 1000, FALSE },
++              { "field < 1weeks", 7LL * 24 * 60 * 60 * 1000 * 1000, FALSE },
+       };
+ 
+       struct event_filter *filter;
diff --git a/patches/fix-man-errors.patch b/patches/fix-man-errors.patch
new file mode 100644 (file)
index 0000000..88c8de6
--- /dev/null
@@ -0,0 +1,72 @@
+Description: Fix groff errors in upstream manpages
+Author: Noah Meyerhans <noahm@debian.org>
+Forwarded: no
+Last-Update: 2025-05-02
+---
+This patch header follows DEP-3: http://dep.debian.net/deps/dep3/
+Index: dovecot/doc/man/dovecot-lda.1
+===================================================================
+--- dovecot.orig/doc/man/dovecot-lda.1
++++ dovecot/doc/man/dovecot-lda.1
+@@ -1,5 +1,6 @@
+-.TH "" "" "March 2025" "78ffb79" "Dovecot"
+-dovecot-lda - Dovecot's local mail delivery agent
++.TH "DOVECOT-LDA" "1" "March 2025" "78ffb79" "Dovecot"
++.SH "NAME"
++\fBdovecot-lda\fR - Dovecot's local mail delivery agent
+ .SH "SYNOPSIS"
+ .P
+ \fBdovecot-lda\fR \[lB]\fB-ek\fR\[rB] \[lB]\fB-a\fR \fIaddress\fR\[rB] \[lB]\fB-c\fR \fIconfig_file\fR\[rB] \[lB]\fB-d\fR \fIusername\fR\[rB] \[lB]\fB-f\fR \fIenvelope_sender\fR\[rB] \[lB]\fB-m\fR \fImailbox\fR\[rB] \[lB]\fB-o\fR \fIsetting=value\fR\[rB] \[lB]\fB-p\fR \fIpath\fR\[rB] \[lB]\fB-r\fR \fIaddress\fR\[rB]
+Index: dovecot/doc/man/doveadm-deduplicate.1
+===================================================================
+--- dovecot.orig/doc/man/doveadm-deduplicate.1
++++ dovecot/doc/man/doveadm-deduplicate.1
+@@ -6,7 +6,7 @@
+ \fBdoveadm\fR \[lB]\fIGLOBAL OPTIONS\fR\[rB] \fBdeduplicate\fR \[lB]\fB-u\fR \fIuser\fR | \fB-A\fR | \fB-F\fR \fIfile\fR | \fB--no-userdb-lookup\fR\[rB] \[lB]\fB-S\fR \fIsocket_path\fR\[rB] \[lB]\fB-m\fR\[rB] \fIsearch_query\fR
+ .SH "DESCRIPTION"
+ .P
+-. doveadm(1) will delete the newest duplicated messages from the mailbox and keep the oldest.
++doveadm(1) will delete the newest duplicated messages from the mailbox and keep the oldest.
+ .P
+ Deduplication across multiple mailboxes is not supported.
+ .SH "GLOBAL OPTIONS"
+@@ -157,7 +157,7 @@ Run the \fIcommand\fR only for the given
+ .RS 0
+ .RS 4
+ .P
+-' will be sufficient. See doveadm-search-query(7) for details.
++will be sufficient. See doveadm-search-query(7) for details.
+ .RE 0
+ 
+ .RE 0
+@@ -179,7 +179,7 @@ guid                             uid
+ 8aad0f0a30169f4bea620000ca356bad 18756
+ 923e301ab9219b4b4f440000ca356bad 18748
+ 923e301ab9219b4b4f440000ca356bad 18753
+-...
++\fR...
+ .fi
+ .RE
+ .P
+@@ -196,7 +196,7 @@ guid                             uid
+ 8aad0f0a30169f4bea620000ca356bad 18751
+ 923e301ab9219b4b4f440000ca356bad 18748
+ a7999e1530739c4bd26d0000ca356bad 18749
+-...
++\fR...
+ .fi
+ .RE
+ .SH "REPORTING BUGS"
+Index: dovecot/doc/man/doveadm-fetch.1
+===================================================================
+--- dovecot.orig/doc/man/doveadm-fetch.1
++++ dovecot/doc/man/doveadm-fetch.1
+@@ -377,7 +377,7 @@ The date-time attribute when present, ot
+ .RS 0
+ .RS 4
+ .P
+-Fetch messages matching this search query. See doveadm-search-query(1) for details.
++Fetch messages matching this search query. See doveadm-search-query(7) for details.
+ .RE 0
+ 
+ .RE 0
diff --git a/patches/lib-mail-istream-header-filter-Fix-potential-assert-.patch b/patches/lib-mail-istream-header-filter-Fix-potential-assert-.patch
new file mode 100644 (file)
index 0000000..6cb6c9c
--- /dev/null
@@ -0,0 +1,84 @@
+From 3ff6022baa757583faf579251fc4033f373d4244 Mon Sep 17 00:00:00 2001
+From: Timo Sirainen <timo.sirainen@open-xchange.com>
+Date: Tue, 2 Sep 2025 13:28:44 +0300
+Subject: [PATCH] lib-mail: istream-header-filter - Fix potential assert-crash
+
+In some situations multiple snapshots were created, which broke the logic
+of using a single snapshot_pending boolean. Changed it to refcount.
+
+Fixes:
+Panic: file istream-header-filter.c: line 665 (i_stream_header_filter_snapshot_free): assertion failed: (snapshot->mstream->snapshot_pending)
+---
+ src/lib-mail/istream-header-filter.c | 14 +++++++-------
+ 1 file changed, 7 insertions(+), 7 deletions(-)
+
+diff --git a/src/lib-mail/istream-header-filter.c b/src/lib-mail/istream-header-filter.c
+index ee94e4f82b..2d938bdaad 100644
+--- a/src/lib-mail/istream-header-filter.c
++++ b/src/lib-mail/istream-header-filter.c
+@@ -27,6 +27,7 @@ struct header_filter_istream {
+       void *context;
+ 
+       buffer_t *hdr_buf;
++      int snapshot_pending_refcount;
+       struct message_size header_size;
+       uoff_t skip_count;
+       uoff_t last_lf_offset;
+@@ -50,7 +51,6 @@ struct header_filter_istream {
+       bool eoh_not_matched:1;
+       bool callbacks_called:1;
+       bool prev_matched:1;
+-      bool snapshot_pending:1;
+ };
+ 
+ header_filter_callback *null_header_filter_callback = NULL;
+@@ -66,7 +66,7 @@ static void i_stream_header_filter_destroy(struct iostream_private *stream)
+               message_parse_header_deinit(&mstream->hdr_ctx);
+       if (array_is_created(&mstream->match_change_lines))
+               array_free(&mstream->match_change_lines);
+-      if (!mstream->snapshot_pending)
++      if (mstream->snapshot_pending_refcount == 0)
+               buffer_free(&mstream->hdr_buf);
+       else {
+               /* Clear hdr_buf to make sure
+@@ -168,7 +168,7 @@ static ssize_t hdr_stream_update_pos(struct header_filter_istream *mstream)
+ 
+ static void hdr_buf_realloc_if_needed(struct header_filter_istream *mstream)
+ {
+-      if (!mstream->snapshot_pending)
++      if (mstream->snapshot_pending_refcount == 0)
+               return;
+ 
+       /* hdr_buf exists in a snapshot. Leave it be and create a copy of it
+@@ -177,7 +177,7 @@ static void hdr_buf_realloc_if_needed(struct header_filter_istream *mstream)
+       mstream->hdr_buf = buffer_create_dynamic(default_pool,
+                                                I_MAX(1024, old_buf->used));
+       buffer_append(mstream->hdr_buf, old_buf->data, old_buf->used);
+-      mstream->snapshot_pending = FALSE;
++      mstream->snapshot_pending_refcount = 0;
+ 
+       mstream->istream.buffer = mstream->hdr_buf->data;
+ }
+@@ -662,8 +662,8 @@ i_stream_header_filter_snapshot_free(struct istream_snapshot *_snapshot)
+       if (snapshot->mstream->hdr_buf != snapshot->hdr_buf)
+               buffer_free(&snapshot->hdr_buf);
+       else {
+-              i_assert(snapshot->mstream->snapshot_pending);
+-              snapshot->mstream->snapshot_pending = FALSE;
++              i_assert(snapshot->mstream->snapshot_pending_refcount > 0);
++              snapshot->mstream->snapshot_pending_refcount--;
+       }
+       i_free(snapshot);
+ }
+@@ -687,7 +687,7 @@ i_stream_header_filter_snapshot(struct istream_private *stream,
+       snapshot->hdr_buf = mstream->hdr_buf;
+       snapshot->snapshot.free = i_stream_header_filter_snapshot_free;
+       snapshot->snapshot.prev_snapshot = prev_snapshot;
+-      mstream->snapshot_pending = TRUE;
++      mstream->snapshot_pending_refcount++;
+       return &snapshot->snapshot;
+ }
+ 
+-- 
+2.47.3
+
diff --git a/patches/lib-sieve_sieve-script_c_sieve_script_create_common_Correctly_handle_errors.patch b/patches/lib-sieve_sieve-script_c_sieve_script_create_common_Correctly_handle_errors.patch
new file mode 100644 (file)
index 0000000..40d1e17
--- /dev/null
@@ -0,0 +1,26 @@
+From 2b9a4e85bc4547fcff84398b2bda00d0ced09fae Mon Sep 17 00:00:00 2001
+From: Alexander Gerasiov <a@gerasiov.net>
+Date: Tue, 23 Sep 2025 13:50:43 +0300
+Subject: [PATCH] lib-sieve/sieve-script.c: sieve_script_create_common:
+ Correctly handle errors.
+
+Fixes null pointer deref (e.g. in case of absent file).
+---
+ src/lib-sieve/sieve-script.c | 4 ++--
+ 1 file changed, 2 insertions(+), 2 deletions(-)
+
+Index: dovecot/pigeonhole/src/lib-sieve/sieve-script.c
+===================================================================
+--- dovecot.orig/pigeonhole/src/lib-sieve/sieve-script.c
++++ dovecot/pigeonhole/src/lib-sieve/sieve-script.c
+@@ -166,8 +166,8 @@ sieve_script_create_common(struct sieve_
+               sieve_storage_unref(&storage);
+               if (ret > 0 && open &&
+                   sieve_script_open(script, error_code_r) < 0) {
+-                      *error_r = sieve_storage_get_last_error(
+-                              storage, error_code_r);
++                      *error_r = sieve_script_get_last_error(
++                              script, error_code_r);
+                       sieve_script_unref(&script);
+                       if (*error_code_r == SIEVE_ERROR_NOT_FOUND)
+                               continue;
diff --git a/patches/series b/patches/series
new file mode 100644 (file)
index 0000000..328862f
--- /dev/null
@@ -0,0 +1,212 @@
+#split-protocols.patch
+#fix-mail_plugin_dir-default.patch
+#ssl-cert-location.patch
+#tcpwrapper.patch
+#default-mail_location.patch
+#mboxlocking.patch
+#dovecot_name.patch
+#ssl-dh-params-location.patch
+skip-rfc-subdir.patch
+#Correct-misspellings.patch
+#test-backtrace.patch
+#doveadm-director.1-drop-acute-accent.patch
+#Fix-32bit-sign-comparisons.patch
+#Improve-cross-compile-support.patch
+#Silence-LTO-related-compiler-warning.patch
+#md4-md5-disable-optimization-causing-unaligned-access.patch
+#Support-openssl-3.0.patch
+#Debug-flaky-unit-test.patch
+#auth-Fix-handling-passdbs-with-identical-driver-args-but-.patch
+#auth-Add-a-comment-about-updating-userdb_find.patch
+#Fix-uninitialized-read-in-doveadm-oldstats.patch
+0001-lda-Fix-using-USER-environment-if-d-hasn-t-been-spec.patch
+0002-lda-Default-mail_home-HOME-environment-if-not-using-.patch
+Use-_FORTIFY_SOURCE-level-3.patch
+fit-32-bit-test-integers.patch
+bug1104549-gssapi-regression.patch
+fix-man-errors.patch
+bug1106784_Fix-LDAP-SASL-auth-support.patch
+auth__Use_AUTH_CACHE_KEY_USER_instead_of_per-database.patch
+auth__Terminate_properly_auth_oauth2_post_setting_defines.patch
+lib-sieve_sieve-script_c_sieve_script_create_common_Correctly_handle_errors.patch
+bug1121000_dovecot-ldap_Crash_if_iterate_filter_is_set_but_iterate_fields_is_not_set.patch
+0001-trash-Use-mailbox-event-in-trash_try_mailbox-for-set.patch
+acl-Fix-crash-when-group-ACLs-are-used-but-user-s-ac.patch
+CVE-2025-59028.patch
+CVE-2025-59031.patch
+CVE-2025-59032.patch
+CVE-2026-24031-27860-1.patch
+CVE-2026-24031-27860-2.patch
+CVE-2026-24031-27860-3.patch
+CVE-2026-24031-27860-4.patch
+CVE-2026-24031-27860-5.patch
+CVE-2026-24031-27860-6.patch
+CVE-2026-24031-27860-7.patch
+CVE-2026-24031-27860-8.patch
+CVE-2026-27855-1.patch
+CVE-2026-27855-2.patch
+CVE-2026-27855-3.patch
+CVE-2026-27855-4.patch
+CVE-2026-27856-1.patch
+CVE-2026-27856-2.patch
+CVE-2026-27856-3.patch
+CVE-2026-27857-1.patch
+CVE-2026-27857-2.patch
+CVE-2026-27857-3.patch
+CVE-2026-27857-4.patch
+CVE-2026-27857-5.patch
+CVE-2026-27858.patch
+CVE-2026-27859.patch
+CVE-2026-27851.patch
+CVE-2026-40016.patch
+CVE-2026-33603.patch
+CVE-2026-40020-1.patch
+CVE-2026-40020-2.patch
+CVE-2026-40020-3.patch
+CVE-2026-42006.patch
+lib-mail-istream-header-filter-Fix-potential-assert-.patch
+# CVE-2026-52687
+0001-imap-Restrict-COMPRESS-to-DEFLATE-only.patch
+0002-doveadm-compress-connect-Don-t-hang-when-server-reje.patch
+0003-doveadm-compress-Delay-failing-if-used-COMPRESS-algo.patch
+# CVE-2026-52681
+0001-lib-storage-Allow-NULL-path-in-mailbox_list_mkdir_ro.patch
+0002-lib-storage-Simplify-mailbox_mkdir-using-mailbox_lis.patch
+0001-lib-sieve-sieve-storage-Add-is_personal-bit.patch
+0001-lib-sieve-sieve-storage-Fix-panic-caused-by-flawed-d.patch
+0001-lib-sieve-Add-per-user-sieve-rusage-file-for-cumulat.patch
+0002-lib-sieve-Drop-binary-header-resource-usage-tracking.patch
+0003-lib-sieve-Rename-on-disk-header-rusage-fields-to-unu.patch
+0004-lib-sieve-Switch-sieve_max_cpu_time-enforcement-to-t.patch
+0005-lib-sieve-file-storage-Remove-orphan-compiled-binari.patch
+0006-lib-sieve-Log-a-warning-when-cumulative-CPU-exceeds-.patch
+0007-lib-sieve-Log-an-info-line-when-opening-a-CPU-disabl.patch
+0008-lib-sieve-Reduce-CPU-limit-check-interval-in-contain.patch
+# CVE-2026-42391
+0001-imap-login-Add-comments-to-internal-ID-command-param.patch
+0001-imap-login-Fix-excessive-memory-growth-with-pre-logi.patch
+# CVE-2026-40204
+0001-lib-lda-mail-deliver-Autocreate-delivery-mailbox-via.patch
+0002-lib-storage-Remove-MAILBOX_FLAG_AUTO_CREATE-and-MAIL.patch
+0001-lib-sieve-actions-Split-mailbox-creation-into-sieve_.patch
+0002-lib-sieve-actions-Create-fileinto-mailbox-without-th.patch
+# CVE-2026-42394
+0001-lib-i_close_fd-Document-that-it-preserves-errno.patch
+0002-lib-path-util-Add-t_openat_safe-for-jailed-openat-re.patch
+0003-lib-Add-t_openat_safe_dir.patch
+0004-acl-dovecot-acl-list-is-never-supposed-to-be-a-symli.patch
+0005-lib-Add-comments-about-memory-allocations-and-string.patch
+0001-lib-sieve-storage-file-Refuse-symlinks-escaping-pers.patch
+0002-lib-sieve-storage-file-Validate-script-stat-path-thr.patch
+# CVE-2026-73209
+0004-lib-compression-test-Add-zero-len-frame-test-for-all.patch
+0003-lib-compression-istream-zstd-Guard-against-no-progre.patch
+0001-lib-compression-istream-lz4-Ensure-uncompressed-chun.patch
+0001-lib-compression-istream-lz4-Try-again-if-no-data-was.patch
+0001-lib-compression-Don-t-calculate-crc32-if-using-zlib-.patch
+0001-lib-compression-istream-zlib-Use-container_of-macro.patch
+0001-global-Fix-spelling.patch
+0001-lib-compression-wrap-_read-in-for-loop-reindent-only.patch
+0002-lib-compression-Iterate-instead-of-recursing-on-zero.patch
+# CVE-2026-40018
+0001-lib-sql-sql-api-Implement-async-calls-for-drivers-th.patch
+0001-lib-sql-Add-sql_result_new_error-helper.patch
+0002-lib-sql-Extract-sql_query_callback_delayed-helper.patch
+0003-lib-sql-Add-sql_commit_schedule_delayed-helper.patch
+0004-lib-var-expand-Change-escape-func-signature-to-retur.patch
+0001-lib-sql-driver-sqlite-Use-sqlite3_snprintf-to-quote-.patch
+0001-lib-sql-Use-strchr-based-while-loop-to-fill-template.patch
+0005-lib-sql-Make-escape_string-return-int-with-error_r-f.patch
+0006-auth-passdb_sql-connect-before-expanding-query-varia.patch
+0007-lib-sql-Run-sql-statement-queries-in-their-own-data-.patch
+# CVE-2026-40205 and CVE-2026-73208
+0001-auth-db-oauth2-Reduce-nesting-in-token_in_scope-via-.patch
+0002-auth-db-oauth2-Fix-scope-check-to-require-all-config.patch
+0003-auth-db-oauth2-Add-oauth2_audience-setting-deprecate.patch
+# CVE-2026-33604
+0001-lib-mail-ostream-dot-Optimize-stream-writing.patch
+0001-lib-mail-o_stream_dot_sendv-Use-I_MIN.patch
+0002-lib-mail-o_stream_dot_sendv-Fix-boundary-off-by-one.patch
+0003-lib-mail-o_stream_dot_sendv-Use-enumeration-to-defin.patch
+0001-lib-test-Add-test_assert_memcmp.patch
+0001-lib-test-Require-both-lengths-in-test_assert_memcmp.patch
+0004-lib-mail-o_stream_dot_sendv-Do-not-send-unguarded-.-.patch
+# CVE-2026-33607
+0001-lib-imap-imap-match-Fix-excessive-CPU-usage-caused-b.patch
+# CVE-2026-27852
+0001-lib-mail-Reset-charset-translation-buffer-between-MI.patch
+0002-imap-Stream-BODYSTRUCTURE-to-client-without-ostream-.patch
+0003-imap-Stream-ENVELOPE-to-client-without-ostream-memor.patch
+0004-lib-mail-message-parser-from-parts-Enforce-50-MB-all.patch
+0005-lib-mail-Add-count-field-to-struct-message_address_l.patch
+0006-lib-storage-mbox-Explicitly-disable-the-header-block.patch
+0007-lib-mail-istream-header-filter-Use-container_of-for-.patch
+0008-lib-mail-Make-istream-header-filter-s-per-header-siz.patch
+0009-lib-storage-Cap-per-header-size-in-index_mail_get_he.patch
+0010-lib-mail-Clamp-hdr-value-cumulatively-against-header.patch
+0011-lib-mail-Introduce-struct-message_part_data_limits-a.patch
+0012-lib-mail-Limit-total-address-count-per-message-to-10.patch
+0013-lib-mail-Limit-total-Content-Language-tag-count-per-.patch
+0014-lib-mail-Limit-total-MIME-parameter-count-per-messag.patch
+# CVE-2026-40014
+0001-lib-storage-thread-Avoid-excessive-data-stack-growth.patch
+0002-lib-storage-thread-Limit-References-header-msgid-cou.patch
+0003-lib-storage-thread-Limit-ancestor-chain-traversal-de.patch
+# CVE-2026-40013
+0001-lib-sieve-sieve-binary-code-Fix-single-byte-oob-stac.patch
+# CVE-2026-33605
+0001-lib-managesieve-managesieve-parser-Fix-handling-of-l.patch
+0002-managesieve-login-client_skip_line-Discard-data-when.patch
+# CVE-2026-40015
+0001-imap-hibernate-Fix-out-of-bounds-read-when-parsing-D.patch
+0002-imap-hibernate-Use-imap-parser-API-for-parsing-DONE-.patch
+# CVE-2026-40017
+0001-lib-Add-XXH64-hash-implementation.patch
+0001-lib-xxh64-fix-byte-ordering-bug-on-big-endian-system.patch
+0002-lib-Replace-str_hash-strcase_hash-with-xxh64.patch
+0003-lib-Add-str_stable_hash.patch
+0004-lib-Key-str_hash-strcase_hash-with-a-random-seed-to-.patch
+0005-lib-index-mail-index-strmap-Fix-OOB-read-from-trunca.patch
+0006-lib-index-Rename-crc32-variables-parameters-to-hash-.patch
+0007-lib-index-Add-keyed-xxh64-strmap-format-v2-gated-by-.patch
+0008-doveadm-Fix-ubsan-unsigned-integer-overflow-error.patch
+0009-config-Assume-dovecot_config_version-0.0.0-is-the-sa.patch
+0010-lib-master-Move-config_version_find-from-src-config-.patch
+0011-lib-lib-master-Move-version_-to-lib-version.-ch.patch
+0012-config-doveconf-dF-Add-dovecot_storage_version.patch
+# CVE-2026-42007
+0001-lib-sieve-util-edit-mail-Fix-writing-to-freed-memory.patch
+0002-lib-sieve-util-test-edit-mail-Adjust-to-changes-in-d.patch
+0003-lib-sieve-edit_mail_headers_parse-Fix-info-leak-via-.patch
+# CVE-2026-33263
+0001-login-common-client-common-Add-client_disconnect-vfu.patch
+0002-submission-login-client-Fix-panic-occurring-at-mail_.patch
+0003-lib-smtp-smtp-server-Expose-smtp_server_connection_r.patch
+0004-submission-login-client-authenticate-Reply-421-4.7.0.patch
+0001-commin-Rename-LOGIN_PROXY_FAILURE_TYPE_AUTH-to-LOGIN.patch
+0001-managesieve-login-Adjust-to-core-s-login_proxy_failu.patch
+0001-login-common-login-Add-proxy_dest_connection_limit-e.patch
+0005-submission-login-submission-proxy-Recognize-421-4.7..patch
+0001-managesieve-login-Support-LOGIN_PROXY_FAILURE_TYPE_A.patch
+0001-login-Add-LOGIN_PROXY_FAILURE_TYPE_AUTH_NOT_REPLIED.patch
+# CVE-2026-33606
+0001-dsync-Avoid-potentially-excessive-data-stack-growth-.patch
+0002-dsync-Fix-escaping-mail-or-attribute-value-that-begi.patch
+# CVE-2026-40203
+0001-imap-Extract-side-channel-ostream-creation-into-help.patch
+0002-imap-Recreate-multiplex-ostream-side-channel-after-u.patch
+0003-lib-compression-Add-o_stream_deflate_reset_dict.patch
+0004-imap-login-Add-dict_reset-side-channel-command-to-im.patch
+# CVE-2026-42008
+0005-imap-Add-imap_compress_on_proxy-hidden-setting.patch
+0006-imap-Reset-DEFLATE-dictionary-after-every-tagged-rep.patch
+0001-submission-Fix-settings-leak-on-error-paths-in-clien.patch
+0002-auth-Fix-prefixing-forward_fields-without-a-value-fr.patch
+# CVE-2026-42392
+0001-lib-imap-urlauth-Fix-leaking-uninitialized-memory-in.patch
+# CVE-2026-42393
+0001-lib-Add-str_equals_timing_safe.patch
+0002-doveadm-Avoid-leaking-doveadm_password-or-doveadm_ap.patch
+# CVE-2026-42395
+0001-login-common-Fix-crash-when-XCLIENT-FORWARD-base64-c.patch
+0002-login-common-Avoid-array_front-panic-on-empty-forwar.patch
diff --git a/patches/skip-rfc-subdir.patch b/patches/skip-rfc-subdir.patch
new file mode 100644 (file)
index 0000000..5f35dc0
--- /dev/null
@@ -0,0 +1,34 @@
+From: Noah Meyerhans <noahm@debian.org>
+Date: Thu, 21 May 2020 21:48:59 -0700
+Subject: Don't try to build doc/rfc subdir components
+Forwarded: not-needed
+---
+ pigeonhole/configure.ac    | 1 -
+ pigeonhole/doc/Makefile.am | 1 -
+ pigeonhole/doc/Makefile.in | 1 -
+ 3 files changed, 3 deletions(-)
+
+Index: dovecot/pigeonhole/configure.ac
+===================================================================
+--- dovecot.orig/pigeonhole/configure.ac
++++ dovecot/pigeonhole/configure.ac
+@@ -258,7 +258,6 @@ doc/Makefile
+ doc/man/Makefile
+ doc/example-config/Makefile
+ doc/example-config/conf.d/Makefile
+-doc/rfc/Makefile
+ doc/extensions/Makefile
+ doc/locations/Makefile
+ doc/plugins/Makefile
+Index: dovecot/pigeonhole/doc/Makefile.am
+===================================================================
+--- dovecot.orig/pigeonhole/doc/Makefile.am
++++ dovecot/pigeonhole/doc/Makefile.am
+@@ -1,7 +1,6 @@
+ SUBDIRS = \
+       man \
+       example-config \
+-      rfc \
+       extensions \
+       locations \
+       plugins
diff --git a/po/nl.po b/po/nl.po
new file mode 100644 (file)
index 0000000..e5c0446
--- /dev/null
+++ b/po/nl.po
@@ -0,0 +1,102 @@
+# Dutch translation of dovecot debconf templates.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the dovecot package.
+# Frans Spiesschaert <Frans.Spiesschaert@yucom.be>, 2014.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: dovecot\n"
+"Report-Msgid-Bugs-To: dovecot@packages.debian.org\n"
+"POT-Creation-Date: 2013-11-07 07:06+0100\n"
+"PO-Revision-Date: 2014-10-04 22:25+0200\n"
+"Last-Translator: Frans Spiesschaert <Frans.Spiesschaert@yucom.be>\n"
+"Language-Team: Debian Dutch l10n Team <debian-l10n-dutch@lists.debian.org>\n"
+"Language: nl\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+#. Type: error
+#. Description
+#: ../dovecot-core.templates:2001
+msgid "Wrong location for SSL certificates"
+msgstr "De SSL-certificaten staan op een verkeerde plaats"
+
+#. Type: error
+#. Description
+#: ../dovecot-core.templates:2001
+msgid ""
+"This machine uses SSL certificates for Dovecot. These certificates should be "
+"moved from /etc/ssl to /etc/dovecot and Dovecot's configuration file (/etc/"
+"dovecot/conf.d/10-ssl.conf) should be updated accordingly."
+msgstr ""
+"Deze computer gebruikt SSL-certificaten voor Dovecot. Deze certificaten "
+"moeten verplaatst worden van /etc/ssl naar /etc/dovecot en het "
+"configuratiebestand van Dovecot (/etc/dovecot/conf.d/10-ssl.conf) moet in "
+"die zin aangepast worden."
+
+#. Type: error
+#. Description
+#: ../dovecot-core.templates:2001
+msgid "Please read /usr/share/doc/dovecot-core/README.Debian.gz for details."
+msgstr ""
+"Raadpleeg /usr/share/doc/dovecot-core/README.Debian.gz voor de details."
+
+#. Type: boolean
+#. Description
+#: ../dovecot-core.templates:3001
+msgid "Create a self-signed SSL certificate?"
+msgstr "Een door uzelf ondertekend SSL-certificaat aanmaken?"
+
+#. Type: boolean
+#. Description
+#: ../dovecot-core.templates:3001
+msgid ""
+"An SSL certificate is needed in order to use IMAP or POP3 over SSL/TLS. No "
+"such certificate was found."
+msgstr ""
+"U heeft een SSL-certificaat nodig om IMAP of POP3 over SSL/TLS te kunnen "
+"gebruiken. Een dergelijk certificaat werd niet gevonden."
+
+#. Type: boolean
+#. Description
+#: ../dovecot-core.templates:3001
+msgid ""
+"Please choose whether you want to create one now. This will then be a self-"
+"signed certificate."
+msgstr ""
+"Geef aan of u nu een dergelijk certificaat wilt aanmaken. In dat geval zal "
+"het een door uzelf ondertekend certificaat zijn."
+
+#. Type: boolean
+#. Description
+#: ../dovecot-core.templates:3001
+msgid ""
+"If you choose not to create a certificate, please adapt Dovecot's "
+"configuration file (/etc/dovecot/conf.d/10-ssl.conf)."
+msgstr ""
+"Indien u ervoor kiest om geen certificaat aan te maken, dan moet u het "
+"configuratiebestand van Dovecot (/etc/dovecot/conf.d/10-ssl.conf) wijzigen."
+
+#. Type: string
+#. Description
+#: ../dovecot-core.templates:4001
+msgid "Host name:"
+msgstr "Computernaam"
+
+#. Type: string
+#. Description
+#: ../dovecot-core.templates:4001
+msgid "Please enter the host name to use in the SSL certificate."
+msgstr ""
+"Geef aan welke computernaam gebruikt moet worden in het SSL-certificaat."
+
+#. Type: string
+#. Description
+#: ../dovecot-core.templates:4001
+msgid ""
+"It will become the \"commonName\" field of the generated SSL certificate."
+msgstr ""
+"Die komt terecht in het veld \"commonName\" van het SSL-certificaat dat "
+"aangemaakt wordt."
diff --git a/rules b/rules
new file mode 100755 (executable)
index 0000000..c13304a
--- /dev/null
+++ b/rules
@@ -0,0 +1,250 @@
+#!/usr/bin/make -f
+
+# Uncomment this to turn on verbose mode.
+#export DH_VERBOSE=1
+
+SHELL=/bin/bash -O extglob
+PIGEONHOLE_DIR=$(CURDIR)/pigeonhole
+
+export DEB_BUILD_MAINT_OPTIONS=hardening=+all optimize=+lto
+
+# LP: 1636781 - strip incompatible default linker option
+ifeq ($(shell dpkg-vendor --derives-from Ubuntu && echo yes),yes)
+  export DEB_LDFLAGS_MAINT_STRIP = -Wl,-Bsymbolic-functions
+endif
+
+DOV_DEB_CFLAGS=
+DOV_DEB_CXXFLAGS=
+DOV_DEB_LDFLAGS=
+
+# Ensure that stacktrace generation works:
+DOV_DEB_LDFLAGS += -rdynamic
+include /usr/share/dpkg/architecture.mk
+ifneq ($(filter armel armhf hppa mips64el mipsel riscv64 sparc64, $(DEB_HOST_ARCH)),)
+  DOV_DEB_CFLAGS += -funwind-tables
+  DOV_DEB_CXXFLAGS += -funwind-tables
+endif
+
+# Extra hardening flags, not for hppa.
+ifeq ($(filter hppa, $(DEB_HOST_ARCH)),)
+  DOV_DEB_CFLAGS += -fstack-clash-protection
+  DOV_DEB_CXXFLAGS += -fstack-clash-protection
+  DOV_DEB_LDFLAGS += -fstack-clash-protection
+endif
+
+export DEB_CFLAGS_MAINT_APPEND = $(DOV_DEB_CFLAGS)
+export DEB_CXXFLAGS_MAINT_APPEND = $(DOV_DEB_CXXFLAGS)
+export DEB_LDFLAGS_MAINT_APPEND = $(DOV_DEB_LDFLAGS)
+
+
+ifeq ($(DEB_HOST_ARCH_OS),linux)
+  CONFIGURE_APPARMOR = --with-apparmor
+endif
+
+ifeq (i386,$(DEB_HOST_ARCH))
+  CONFIGURE_Y2038=--disable-year2038
+else
+  CONFIGURE_Y2038=--enable-year2038
+endif
+
+CLEANFILES=src/lib-settings/settings-history-core.c \
+  src/lib-dict-backend/dict-drivers-register.c \
+  src/lib-sql/test-database.db \
+  pigeonhole/src/plugins/settings/pigeonhole-settings-dynamic.c
+
+# Macros to help move files to the module packages
+CORE_DIR=$(CURDIR)/debian/dovecot-core
+PKG_DIR=$(CURDIR)/debian/dovecot-PKG
+MOVE=install -d $(PKG_DIR:PKG=$(2))/$(dir $(1)); mv $(CORE_DIR)/$(1) $(PKG_DIR:PKG=$(2))/$(dir $(1));
+
+# Files to move from dovecot-core to the individual packages. Files added to
+# <addon>_files will be automatically moved from dovecot's tree to the
+# respective package.
+auth-lua_files = usr/lib/dovecot/modules/auth/libauthdb_lua.so
+
+imapd_files = usr/lib/dovecot/imap* \
+             usr/lib/dovecot/modules/lib??_imap_!(*sieve*) \
+             usr/share/dovecot/conf.d/??-imap*
+
+pop3d_files = usr/lib/dovecot/pop3* \
+             usr/share/dovecot/conf.d/??-pop3*
+
+lmtpd_files = usr/lib/dovecot/lmtp* \
+             usr/share/dovecot/conf.d/??-lmtp*
+
+managesieved_files = usr/lib/dovecot/managesieve* \
+                    usr/lib/dovecot/modules/settings/libmanagesieve* \
+                    usr/share/dovecot/conf.d/??-managesieve.conf
+
+submissiond_files = usr/lib/dovecot/submission* \
+                  usr/share/dovecot/conf.d/??-submission.conf
+
+pgsql_files = usr/lib/dovecot/modules/auth/libdriver_pgsql* \
+             usr/lib/dovecot/modules/dict/libdriver_pgsql* \
+             usr/lib/dovecot/modules/libdriver_pgsql*
+
+mysql_files = usr/lib/dovecot/modules/auth/libdriver_mysql* \
+             usr/lib/dovecot/modules/dict/libdriver_mysql* \
+             usr/lib/dovecot/modules/libdriver_mysql*
+
+sqlite_files = usr/lib/dovecot/modules/auth/libdriver_sqlite* \
+              usr/lib/dovecot/modules/dict/libdriver_sqlite* \
+              usr/lib/dovecot/modules/libdriver_sqlite*
+
+gssapi_files = usr/lib/dovecot/modules/auth/libmech_gssapi*
+
+ldap_files = usr/lib/dovecot/modules/auth/libauthdb_ldap* \
+            usr/lib/dovecot/modules/dict/libdict_ldap* \
+            usr/lib/dovecot/libdovecot-ldap* \
+            usr/share/dovecot/*-ldap.conf.ext \
+            usr/share/dovecot/conf.d/*-ldap.conf.ext
+
+sieve_files = usr/bin/sieve* \
+             usr/lib/dovecot/modules/lib??_sieve* \
+             usr/lib/dovecot/modules/lib??_imap_*sieve* \
+             usr/lib/dovecot/modules/doveadm/lib??_doveadm_sieve* \
+             usr/lib/dovecot/modules/sieve \
+             usr/lib/dovecot/libdovecot-sieve* \
+             usr/share/man/man1/sieve* \
+             usr/share/dovecot/conf.d/??-sieve.conf \
+             usr/share/dovecot/conf.d/??-sieve-extprograms.conf
+
+flatcurve_files = usr/lib/dovecot/modules/lib??_fts_flatcurve_* \
+                 usr/share/dovecot/conf.d/??-fts-flatcurve.conf
+
+solr_files = usr/lib/dovecot/modules/lib??_fts_solr_*
+
+dev_files = usr/include/* \
+           usr/lib/dovecot/dovecot-config \
+           usr/share/aclocal/*.m4
+
+%:
+       dh $@
+
+override_dh_autoreconf:
+       dh_autoreconf debian/autogen.sh
+
+override_dh_auto_configure: TEST_DIR_OVERRIDE=$(shell mktemp -d)
+override_dh_auto_configure:
+       TEST_DIR_OVERRIDE='$(TEST_DIR_OVERRIDE)' KRB5CONFIG=krb5-config.mit systemdsystemunitdir=/usr/lib/systemd/system dh_auto_configure -- \
+                   --enable-experimental-mail-utf8 \
+                   --with-ldap=plugin \
+                   --with-ssl=openssl \
+                   --with-sql=plugin \
+                   --with-pgsql \
+                   --with-mysql \
+                   --with-sqlite \
+                   --with-gssapi=plugin \
+                   --with-solr \
+                   --with-flatcurve \
+                   --with-ioloop=best \
+                   --with-icu \
+                   $(CONFIGURE_APPARMOR) \
+                   --with-lz4 \
+                   --with-lua=plugin \
+                   --with-sodium \
+                   --libdir=\$${prefix}/lib \
+                   --libexecdir=\$${prefix}/lib \
+                   --docdir=\$${prefix}/share/doc/dovecot-core \
+                   --with-moduledir=\$${prefix}/lib/dovecot/modules \
+                   --with-rundir=/run/dovecot \
+                   --disable-rpath \
+                   --disable-static \
+                   $(CONFIGURE_Y2038)
+
+ifeq (i386,$(DEB_HOST_ARCH))
+       sed -i 's/TIME_T_MAX_BITS 32/TIME_T_MAX_BITS 31/' config.h
+endif
+
+       $(MAKE) dovecot-config
+# remove unreproducible '-ffile-prefix-map=/build/1st/dovecot-2.3.10.1+dfsg1=.'
+#                       '-fdebug-prefix-map=/build/dovecot-O55xqn/dovecot-2.3.10.1+dfsg1=.'
+       sed -i 's/\s\+-ffile-prefix-map=\S\+=.\s\+/ /g' dovecot-config
+       sed -i 's/\s\+-fdebug-prefix-map=\S\+=.\s\+/ /g' dovecot-config
+
+# Pigeonhole
+       touch $(PIGEONHOLE_DIR)/stamp.h.in
+       dh_auto_configure -D $(PIGEONHOLE_DIR) -- \
+               --with-dovecot=../ \
+               --libdir=\$${prefix}/lib \
+               --libexecdir=\$${prefix}/lib \
+               --disable-static \
+               --with-ldap=plugin
+
+override_dh_auto_build:
+# Remove all-settings.c and let it be re-generated from the (patched)
+# component settings files.
+       rm -f src/config/all-settings.c
+# Create doc/stamp-man, which may have been removed on an earlier
+# 'clean' invocation, so we don't try to download docs content from
+# the internet:
+       touch doc/stamp-man
+       dh_auto_build
+       dh_auto_build -D $(PIGEONHOLE_DIR)
+
+override_dh_auto_clean:
+       dh_auto_clean
+       dh_auto_clean -D $(PIGEONHOLE_DIR)
+       -rm -f $(CLEANFILES)
+
+override_dh_auto_install:
+# Install everything under dovecot-core
+       $(MAKE) install DESTDIR=$(CORE_DIR)
+       $(MAKE) -C $(PIGEONHOLE_DIR) install DESTDIR=$(CORE_DIR)
+       rm `find $(CURDIR)/debian -name '*.la'`
+
+override_dh_install:
+# dh_auto_install has installed everything in the dovecot-core package.
+# Run dh_install to install additional files from the source directory
+# or debian/ to the individual packages. NOTE: there is nothing in
+# debian/tmp!
+       dh_install
+       install -D -m 0755 $(CORE_DIR)/usr/share/doc/dovecot-core/mkcert.sh \
+               $(CORE_DIR)/usr/share/dovecot/mkcert.sh
+       rm $(CORE_DIR)/usr/share/doc/dovecot-core/mkcert.sh
+       rm $(CORE_DIR)/usr/share/doc/dovecot-core/dovecot-openssl.cnf
+       rm $(CORE_DIR)/usr/share/doc/dovecot-core/solr-*.xml
+
+# We want the dovecot.conf managed by ucf, not the upstream default:
+       rm -f $(CORE_DIR)/etc/dovecot/dovecot.conf
+       cp -r debian/conf/* $(CORE_DIR)/usr/share/dovecot/
+       $(foreach package,$(patsubst dovecot-%,%,$(shell dh_listpackages)),\
+               $(foreach file,$($(package)_files),\
+                       $(call MOVE,$(file),$(package))))
+
+       rmdir $(CORE_DIR)/usr/include
+       rmdir $(CORE_DIR)/usr/share/aclocal
+       install -d $(PKG_DIR:PKG=dev)/usr/share/dovecot
+       grep ABI_VERSION config.h | awk '{print $$NF}' | tr -d \" \
+               | tr '[A-Z]' '[a-z]' > $(PKG_DIR:PKG=dev)/usr/share/dovecot/dovecot-abi
+# sanity check the ABI:
+       grep -F -q abi $(PKG_DIR:PKG=dev)/usr/share/dovecot/dovecot-abi
+
+# Install apport hook
+       install -D -m 644 debian/source_dovecot.py $(CURDIR)/debian/dovecot-core/usr/share/apport/package-hooks/dovecot-core.py
+
+       install -D -m644 debian/dovecot-imapd.ufw.profile debian/dovecot-imapd/etc/ufw/applications.d/dovecot-imapd
+       install -D -m644 debian/dovecot-pop3d.ufw.profile debian/dovecot-pop3d/etc/ufw/applications.d/dovecot-pop3d
+
+override_dh_installpam:
+       dh_installpam -pdovecot-core --name=dovecot
+
+override_dh_installinit:
+       dh_installinit -pdovecot-core --name=dovecot -u"defaults 20"
+
+override_dh_installsystemd:
+       dh_installsystemd -pdovecot-core dovecot.service
+       dh_installsystemd -pdovecot-core --no-enable dovecot.socket
+
+override_dh_gencontrol:
+       dh_gencontrol -- -Vdovecot:ABI-Version=$(shell cat $(CURDIR)/debian/dovecot-dev/usr/share/dovecot/dovecot-abi)
+
+override_dh_makeshlibs:
+# Do not add an ldconfig trigger; none of the dovecot shared libraries
+# are public.
+       dh_makeshlibs -n
+
+override_dh_installdocs:
+       dh_installdocs
+       cp pigeonhole/README    $(CORE_DIR)/usr/share/doc/dovecot-core/pigeonhole.README
+       cp pigeonhole/NEWS      $(CORE_DIR)/usr/share/doc/dovecot-core/pigeonhole.NEWS
diff --git a/salsa-ci.yml b/salsa-ci.yml
new file mode 100644 (file)
index 0000000..aa4b356
--- /dev/null
@@ -0,0 +1,9 @@
+---
+include:
+  - https://salsa.debian.org/salsa-ci-team/pipeline/raw/master/recipes/debian.yml
+
+# The test suite does not pass reprotest
+variables:
+  SALSA_CI_DISABLE_REPROTEST: 1
+  SALSA_CI_DISABLE_CROSSBUILD_ARM64: 1
+  SALSA_CI_DISABLE_BUILD_PACKAGE_TWICE: 1
diff --git a/source/format b/source/format
new file mode 100644 (file)
index 0000000..163aaf8
--- /dev/null
@@ -0,0 +1 @@
+3.0 (quilt)
diff --git a/source/lintian-overrides b/source/lintian-overrides
new file mode 100644 (file)
index 0000000..d4d4b5b
--- /dev/null
@@ -0,0 +1 @@
+dovecot source: intra-source-package-circular-dependency dovecot-core dovecot-sieve
diff --git a/source_dovecot.py b/source_dovecot.py
new file mode 100644 (file)
index 0000000..84fb839
--- /dev/null
@@ -0,0 +1,38 @@
+#!/usr/bin/python3
+
+'''Dovecot Apport interface
+
+Copyright (C) 2010 Canonical Ltd/
+Author: Chuck Short <chuck.short@canonical.com>
+
+This program is free software; you can redistribute it and/or modify it
+under the terms of the GNU General Public License as published by the
+Free Software Foundation; either version 2 of the License, or (at your
+option) any later version.  See http://www.gnu.org/copyleft/gpl.html for
+the full text of the license.
+'''
+
+from apport.hookutils import *
+
+def add_info(report, ui):
+    response = ui.yesno("The output of dovecot -n may help developers diagnose your bug more quickly, however, it may contain sensitive information. Do you want to include it in your bug report?")
+
+    if response is None: #user canceled
+        raise StopIteration
+
+    elif response:
+        report['DovecotConf'] = root_command_output(['/usr/sbin/dovecot', '-n'])
+
+
+    elif not response:
+        ui.information("The contents of dovecot -n will NOT be includeded in the bug report")
+
+    packages = ['dovecot-common', 'dovecot-core', 'dovecot-dev', 'dovecot-pop3d', 'dovecot-imapd', 'mail-stack-delivery', 'dovecot-postfix']
+    versions = ''
+    for package in packages:
+        try:
+            version = package.get_version(package)
+        except:
+            version = 'N/A'
+        versions += '%s %s\n' %(package, version)
+    report['DovecotInstalledVersions'] = versions
diff --git a/tests/control b/tests/control
new file mode 100644 (file)
index 0000000..fb703d6
--- /dev/null
@@ -0,0 +1,14 @@
+Tests: doveadm
+Depends: dovecot-core
+Restrictions: needs-root
+
+Tests: systemd
+Depends: dovecot-core, systemd-sysv
+
+Test-Command: run-parts --report --exit-on-error debian/tests/usage
+Depends: dovecot-imapd, dovecot-pop3d, dovecot-managesieved, sieve-connect, python3
+Restrictions: needs-root, breaks-testbed, allow-stderr
+
+Tests: testmails
+Restrictions: needs-root, breaks-testbed
+Depends: dovecot-imapd, dovecot-pop3d, lsb-release, python3, python3-passlib
diff --git a/tests/doveadm b/tests/doveadm
new file mode 100755 (executable)
index 0000000..9e2bd04
--- /dev/null
@@ -0,0 +1,7 @@
+#!/bin/sh
+
+set -e
+
+echo "Checking if dovecot is running"
+doveadm -f flow instance list | grep -q 'running=yes'
+echo "OK"
diff --git a/tests/systemd b/tests/systemd
new file mode 100755 (executable)
index 0000000..48afbf5
--- /dev/null
@@ -0,0 +1,22 @@
+#!/bin/sh
+
+set -e
+
+echo "Checking whether dovecot.service is enabled"
+systemctl is-enabled dovecot.service
+
+echo "Checking whether dovecot.service is a native unit"
+source=$(systemctl show -pSourcePath dovecot.service | cut -d = -f 2)
+if [ -n "$source" ]; then
+       echo $source
+       exit 1
+else
+       echo "OK (no SourcePath found)"
+fi
+
+echo "Checking whether dovecot.socket is inactive"
+status=$(systemctl show -pActiveState dovecot.socket | cut -d = -f 2)
+echo $status
+if [ "$status" != inactive ]; then
+       exit 1
+fi
diff --git a/tests/testmails b/tests/testmails
new file mode 100755 (executable)
index 0000000..67d5f3b
--- /dev/null
@@ -0,0 +1,254 @@
+#!/usr/bin/python3
+
+import grp
+import imaplib
+import os
+import os.path
+import poplib
+import pwd
+import random
+import string
+import subprocess
+import sys
+import unittest
+
+from passlib.hash import des_crypt
+
+
+def random_string(length):
+    '''Return a random string, consisting of ASCII letters, with given
+    length.'''
+
+    s = ''
+    maxind = len(string.ascii_letters)-1
+    for _ in range(length):
+        s += string.ascii_letters[random.randint(0, maxind)]
+    return s.lower()
+
+
+def login_exists(login):
+    '''Checks whether the given login exists on the system.'''
+
+    try:
+        pwd.getpwnam(login)
+        return True
+    except KeyError:
+        return False
+
+
+def get_distribution():
+    '''Return the name of the Linux Distribution we are running.'''
+    cmd = ['lsb_release', '-si']
+    output = subprocess.check_output(cmd)
+    return output.strip()
+
+
+class TestUser:
+    '''Create a temporary test user and remove it again on close.'''
+
+    def __init__(self):
+        '''Create a new user account with a random password.'''
+
+        self.login = None
+
+        while True:
+            login = random_string(8)
+            if not login_exists(login):
+                break
+
+        self.salt = random_string(2)
+        self.password = random_string(8)
+        self.crypted = des_crypt.using(salt=self.salt).hash(self.password)
+
+        subprocess.check_call(['useradd', '-p', self.crypted, '-m', login])
+
+        self.login = login
+        p = pwd.getpwnam(self.login)
+        self.uid = p[2]
+        self.gid = p[3]
+
+    def __del__(self):
+        '''Remove the created user account.'''
+
+        if self.login:
+            self.close()
+
+    def close(self):
+        '''Remove the created user account.'''
+
+        subprocess.check_call(['userdel', '-f', '-r', self.login])
+        self.login = None
+
+
+class DovecotBasics(unittest.TestCase):
+    '''Base operational tests for Dovecot server.'''
+
+    def setUp(self):
+        '''Create test scenario.
+
+        We want to test the default setup, but pre-setup an mbox on a tmp user
+        '''
+
+        self.distribution = get_distribution()
+        self.user = TestUser()
+
+        # create fresh test mailbox with one new and one old mail
+        self.mailbox = '/var/mail/' + self.user.login
+        self.orig_mbox = '''From test1@test1.com Fri Nov 17 02:21:08 2006
+Date: Thu, 16 Nov 2006 17:12:23 -0800
+From: Test User 1 <test1@test1.com>
+To: Dovecot tester <dovecot@test.com>
+Subject: Test 1
+Status: N
+
+Some really important news.
+
+From test2@test1.com Tue Nov 28 11:29:34 2006
+Date: Tue, 28 Nov 2006 11:29:34 +0100
+From: Test User 2 <test2@test2.com>
+To: Dovecot tester <dovecot@test.com>
+Subject: Test 2
+Status: R
+
+More news.
+
+Get cracking!
+'''
+        with open(self.mailbox, 'w') as f:
+            f.write(self.orig_mbox)
+        os.chown(self.mailbox, self.user.uid, grp.getgrnam('mail')[2])
+        os.chmod(self.mailbox, 0o660)
+
+    def tearDown(self):
+        self.user.close()
+
+    def _test_pop3_proto(self, pop):
+        '''Internal factorization of POP3 protocol checks with an established
+        connection.'''
+
+        # check empty password
+        self.assertEqual(pop.user(self.user.login), b'+OK')
+        self.assertRaises(poplib.error_proto, pop.pass_, '')
+
+        # check wrong password
+        self.assertEqual(pop.user(self.user.login), b'+OK')
+        self.assertRaises(poplib.error_proto, pop.pass_, '123')
+
+        # check correct password
+        self.assertEqual(pop.user(self.user.login), b'+OK')
+        self.assertEqual(pop.pass_(self.user.password), b'+OK Logged in.')
+
+        # check messages
+        self.assertEqual(pop.stat()[0], 2, b'2 available messages')
+        self.assertEqual(pop.list()[1], [b'1 163', b'2 161'])
+        self.assertEqual('\n'.join(l.decode() for l in pop.retr(1)[1]), '''Date: Thu, 16 Nov 2006 17:12:23 -0800
+From: Test User 1 <test1@test1.com>
+To: Dovecot tester <dovecot@test.com>
+Subject: Test 1
+
+Some really important news.''')
+        self.assertEqual('\n'.join(l.decode() for l in pop.retr(2)[1]), '''Date: Tue, 28 Nov 2006 11:29:34 +0100
+From: Test User 2 <test2@test2.com>
+To: Dovecot tester <dovecot@test.com>
+Subject: Test 2
+
+More news.
+
+Get cracking!''')
+
+        self.assertEqual(pop.quit(), b'+OK Logging out.')
+
+    def test_pop3(self):
+        '''Test POP3 protocol.'''
+
+        pop = poplib.POP3('localhost')
+        self.assertEqual(pop.getwelcome(), b'+OK Dovecot ready.')
+
+        self._test_pop3_proto(pop)
+
+    def test_pop3s(self):
+        '''Test POP3S protocol.'''
+
+        pop = poplib.POP3_SSL('localhost')
+        self.assertEqual(pop.getwelcome(), b'+OK Dovecot ready.')
+
+        self._test_pop3_proto(pop)
+
+    def _test_imap_proto(self, imap):
+        '''Internal factorization of IMAP4 protocol checks with an established
+        connection.'''
+
+        # invalid passwords
+        self.assertRaises(imaplib.IMAP4.error, imap.login,
+                          self.user.login, '')
+        self.assertRaises(imaplib.IMAP4.error, imap.login,
+                          self.user.login, '123')
+
+        # correct password
+        imap.login(self.user.login, self.user.password)
+
+        # list mailboxes
+        status, imlist = imap.list()
+        self.assertEqual(status, 'OK')
+        self.assertTrue(imlist[0].decode().endswith('INBOX'))
+
+        # check mails
+        imap.select()
+        self.assertEqual(imap.search(None, 'ALL'), ('OK', [b'1 2']))
+        self.assertEqual(imap.fetch('1', '(FLAGS)'),
+                         ('OK', [b'1 (FLAGS (\\Recent))']))
+        self.assertEqual(imap.fetch('2', '(FLAGS)'),
+                         ('OK', [b'2 (FLAGS (\\Seen \\Recent))']))
+        self.assertEqual(imap.fetch('1', '(BODY[TEXT])')[1][0][1],
+                         b'Some really important news.\r\n')
+        self.assertEqual(imap.fetch('2', '(BODY[TEXT])')[1][0][1],
+                         b'More news.\r\n\r\nGet cracking!')
+
+        self.assertEqual(imap.fetch('1', '(RFC822)')[1],
+                         [(b'1 (RFC822 {163}',
+                           b'''Date: Thu, 16 Nov 2006 17:12:23 -0800\r
+From: Test User 1 <test1@test1.com>\r
+To: Dovecot tester <dovecot@test.com>\r
+Subject: Test 1\r
+\r
+Some really important news.\r
+'''), b')'])
+
+        # delete mail 1
+        self.assertEqual(imap.store('1', '+FLAGS', '\\Deleted')[0], 'OK')
+        self.assertEqual(imap.expunge()[0], 'OK')
+        self.assertEqual(imap.search(None, 'ALL'), ('OK', [b'1']))
+
+        # old mail 2 is mail 1 now
+        self.assertEqual(imap.fetch('1', '(RFC822)')[1],
+                         [(b'1 (RFC822 {161}',
+                           b'''Date: Tue, 28 Nov 2006 11:29:34 +0100\r
+From: Test User 2 <test2@test2.com>\r
+To: Dovecot tester <dovecot@test.com>\r
+Subject: Test 2\r
+\r
+More news.\r
+\r
+Get cracking!'''), b')'])
+        imap.close()
+        imap.logout()
+
+    def test_imap(self):
+        '''Test IMAP4 protocol.'''
+
+        imap = imaplib.IMAP4('localhost')
+        self._test_imap_proto(imap)
+
+    def test_imaps(self):
+        '''Test IMAP4S protocol.'''
+
+        imap = imaplib.IMAP4_SSL('localhost')
+        self._test_imap_proto(imap)
+
+
+if __name__ == '__main__':
+    os.dup2(1, 2)
+    suite = unittest.TestSuite()
+    suite.addTest(unittest.TestLoader().loadTestsFromTestCase(DovecotBasics))
+    result = unittest.TextTestRunner(verbosity=2).run(suite)
+    sys.exit(not result.wasSuccessful())
diff --git a/tests/usage/00_setup b/tests/usage/00_setup
new file mode 100755 (executable)
index 0000000..b802eac
--- /dev/null
@@ -0,0 +1,57 @@
+#!/bin/sh
+
+set -e
+
+echo "Setting up dovecot for the test"
+# Move aside 10-auth.conf to disable passwd-based auth
+if [ -f /etc/dovecot/conf.d/10-auth.conf ]; then
+       mv /etc/dovecot/conf.d/10-auth.conf /etc/dovecot/conf.d/10-auth.conf.bak
+fi
+
+cat >/etc/dovecot/local.conf <<-EOF
+mail_driver = maildir
+mail_path = ~/Maildir
+mail_inbox_path = ~/Maildir
+
+auth_mechanisms = plain
+
+passdb static {
+  driver = static
+  passdb_static_password = test
+}
+
+userdb static {
+  driver = static
+  fields {
+    uid=nobody
+    gid=nogroup
+    home=/srv/dovecot-dep8/%u
+  }
+}
+EOF
+
+mkdir -p /srv/dovecot-dep8
+chown nobody:nogroup /srv/dovecot-dep8
+
+echo "Restarting the service"
+systemctl restart dovecot
+
+echo "Sending a test message via the LDA"
+/usr/lib/dovecot/dovecot-lda -f "test@example.com" -d dep8 <<EOF
+Return-Path: <test@example.com>
+Message-Id: <dep8-test-1@debian.org>
+From: Test User <test@example.com>
+To: dep8 <dep8@example.com>
+Subject: DEP-8 test
+
+This is just a test
+EOF
+
+echo "Verifying that the email was correctly delivered"
+if [ -z "$(doveadm search -u dep8 header message-id dep8-test-1@debian.org)" ]; then
+       echo "Message not found"
+       exit 1
+fi
+
+echo "Done"
+echo
diff --git a/tests/usage/imap b/tests/usage/imap
new file mode 100755 (executable)
index 0000000..5bdede7
--- /dev/null
@@ -0,0 +1,38 @@
+#!/usr/bin/python3
+import imaplib
+
+imaplib.Debug = 4
+
+print("Testing IMAP")
+print("Connecting")
+client = imaplib.IMAP4('localhost')
+
+print("Checking for STARTTLS capability")
+assert 'STARTTLS' in client.capabilities
+
+client.starttls()
+
+print("Logging in")
+client.login('dep8', 'test')
+
+print("Selecting INBOX")
+client.select()
+
+print("Looking for the test message")
+res, uids = client.search(None, 'HEADER', 'MESSAGE-ID', '"<dep8-test-1@debian.org>"')
+
+assert res == 'OK'
+assert len(uids[0]) > 0
+
+uid = uids[0].split()[0]
+
+print("Fetching and verifying test message")
+res, data = client.fetch(uid, '(RFC822)')
+
+assert res == 'OK'
+
+lines = data[0][1].splitlines()
+
+assert b'Subject: DEP-8 test' in lines
+
+print("Done")
diff --git a/tests/usage/pop3 b/tests/usage/pop3
new file mode 100755 (executable)
index 0000000..00b1657
--- /dev/null
@@ -0,0 +1,31 @@
+#!/usr/bin/python3
+import poplib
+
+print("Testing POP3")
+print("Connecting")
+client = poplib.POP3('localhost')
+client.set_debuglevel(2)
+
+print("Checking for STARTTLS capability")
+assert 'STLS' in client.capa()
+
+client.stls()
+
+print("Logging in")
+client.user('dep8')
+client.pass_('test')
+
+print("Listing INBOX")
+res, data, _ = client.list()
+assert res.startswith(b'+OK')
+
+print("Fetching and verifying test message")
+for entry in data:
+    _id, _ = entry.split(maxsplit=1)
+    res, body, _ = client.retr(int(_id))
+    if b'Subject: DEP-8 test' in body:
+        break
+else:
+    raise AssertionError("Test message not found")
+
+print("Done")
diff --git a/tests/usage/sieve b/tests/usage/sieve
new file mode 100755 (executable)
index 0000000..9ef68dd
--- /dev/null
@@ -0,0 +1,8 @@
+#!/usr/bin/bash
+# Validate managesieved authentication
+
+user=dep8
+pass=test
+
+sieve-connect --notlsverify --debug -s localhost \
+             -p 4190 -u "$user" --list --passwordfd 3 3< <(echo "$pass")
diff --git a/upstream/metadata b/upstream/metadata
new file mode 100644 (file)
index 0000000..02236b7
--- /dev/null
@@ -0,0 +1,5 @@
+Name: dovecot
+Repository: https://github.com/dovecot/core
+Repository-Browse: https://github.com/dovecot/core
+Contact: Dovecot Mailing List <dovecot at dovecot.org>
+Bug-Submit: Dovecot Mailing List <dovecot at dovecot.org>
diff --git a/upstream/signing-key.asc b/upstream/signing-key.asc
new file mode 100644 (file)
index 0000000..66bc6ad
--- /dev/null
@@ -0,0 +1,25 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mDMEZ49ZxhYJKwYBBAHaRw8BAQdABhVJFqsoq7gZeXhY/xcz5Vc0i+vM/Df8RDSW
+lzPHsoC0MkRvdmVjb3QgQ29tbXVuaXR5IEVkaXRpb24gPGRvdmVjb3QtY2VAZG92
+ZWNvdC5vcmc+iJAEExYIADgWIQTvCIIHn9TtMr+LI7KhsJ74TtxSGQUCZ49ZxgIb
+AwULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRChsJ74TtxSGUgvAP9gNC6pZKBV
+micPYvE2UOTvX8WoEcjWI1UptTxFsHXOjwEA6DZiPoPn9Vvl4PuyG8TavjDslOL8
+pfn+XKZho2WoAgSJAjMEEAEKAB0WIQQr50qrPudU37nIDTMYo0iu7UCdoQUCZ49a
+xgAKCRAYo0iu7UCdoVPOD/452ZnhEYW2/VxgDmD7DNaaxdfL/vKifc9MP2sJm+a2
+T95zHpm/erV2xw3gPpKcLuMaPhkexf6MMhoh9GnUnwEuoOfMWhMAn26RNPFvuUL0
+a73qoxRY9D+Q/PUNjNsNFj/3c8VIC3DeQhO77wVblLWr9+7J9GrzKtBCkb7zC5Xf
+8zjLkCWZ15S0kVKNvBsZzEzLYVN/+Kk1s1WP4x1gnQdPm8N7+b4XAQSOUXLjkL5S
+vGyIUuHwnYHQJeH51L+q8SpzEr59K9OiVYOCm0ICl28xjOjIC9Ym7a8W5Xa5new3
+ScxNeTuxRItYeDyxXi+YIYGHWYGjmppmlq3VksXirUHfgNoipVSbmGxCukShSsfH
+f/40iO8DRFRnsUN2lzN4TCcT1ayQvKoyMFuoXQJonuLdbc7E+YLNIPsW66+aoAuW
+g+5j2QFMFxAJS9IoTCsVQv52MuCj2X5KgwmBcYSgWGpoog9Yx+aaVQQ+X++9FX8r
+RcnyOGOBaA6htH4g1kUOWwZ/NxObRiA7QLo4Rz6CJrU7ueRIaeSCCXTxCb5dJDeE
+IB1MZh1artKZ6Rko+cv/Vy0Ojb0JfPDDoQyhblPIXhlJN8/4zFCjxilMDA9CTAB6
+kY1TVGp138CDnJHZvCdvbrlWf7ktw3+CGKQsZq96Jf756L28dTA42E+WqCBFMZna
+V7g4BGePWcYSCisGAQQBl1UBBQEBB0A50U//EHUvujYRMNqQ3XMVt8lxU7HtdmsH
+Bamh6BjmLgMBCAeIeAQYFggAIBYhBO8Iggef1O0yv4sjsqGwnvhO3FIZBQJnj1nG
+AhsMAAoJEKGwnvhO3FIZEZQBAKuMI//Cjl38pD68fQfcnIMDV2YnXjvaswe7jbDr
+Jyo4AQDr/VanhFwPxSlZ3MKY4PWyLw4Fm9mTT2ofcluFttRsDg==
+=eQ4x
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/watch b/watch
new file mode 100644 (file)
index 0000000..3d07aed
--- /dev/null
+++ b/watch
@@ -0,0 +1,8 @@
+# watch control file for uscan
+version=4
+# core
+opts="pgpmode=auto, dversionmangle=s/\+dfsg\d*//, uversionmangle=s/\.-[0-9]+//, oversionmangle=s/(.*)/$1+dfsg1/" \
+       https://dovecot.org/releases/2.4/dovecot-([\.0-9]+)(-[0-9]+)?\.tar\.gz debian
+# pigeonhole
+opts="pgpmode=auto, dversionmangle=s/\+dfsg\d*//, uversionmangle=s/\.-[0-9]//, oversionmangle=s/(.*)/$1+dfsg1/, component=pigeonhole" \
+       https://pigeonhole.dovecot.org/releases/2.4/dovecot-pigeonhole-([\.0-9]+)(-[0-9]+)?\.tar\.gz ignore uupdate